mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-01 21:47:50 +02:00
Upstream merge
This commit is contained in:
@ -39,9 +39,7 @@ open class App : Application() {
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
if (BuildConfig.DEBUG) {
|
||||
MultiDex.install(this)
|
||||
}
|
||||
MultiDex.install(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
|
@ -107,6 +107,8 @@ object PreferenceKeys {
|
||||
|
||||
const val defaultCategory = "default_category"
|
||||
|
||||
const val skipRead = "skip_read"
|
||||
|
||||
const val downloadBadge = "display_download_badge"
|
||||
|
||||
@Deprecated("Use the preferences of the source")
|
||||
|
@ -168,6 +168,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||
|
||||
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||
|
||||
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
|
||||
|
||||
fun trustedSignatures() = rxPrefs.getStringSet("trusted_signatures", emptySet())
|
||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
||||
import eu.kanade.tachiyomi.data.track.shikomori.Shikomori
|
||||
|
||||
class TrackManager(private val context: Context) {
|
||||
|
||||
@ -11,6 +12,7 @@ class TrackManager(private val context: Context) {
|
||||
const val MYANIMELIST = 1
|
||||
const val ANILIST = 2
|
||||
const val KITSU = 3
|
||||
const val SHIKOMORI = 4
|
||||
}
|
||||
|
||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
||||
@ -19,7 +21,9 @@ class TrackManager(private val context: Context) {
|
||||
|
||||
val kitsu = Kitsu(context, KITSU)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu)
|
||||
val shikomori = Shikomori(context, SHIKOMORI)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikomori)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
|
@ -7,9 +7,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import okhttp3.HttpUrl
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import java.net.URI
|
||||
|
||||
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
@ -114,23 +114,23 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).delete()
|
||||
networkService.cookies.remove(URI(BASE_URL))
|
||||
networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
|
||||
}
|
||||
|
||||
override val isLogged: Boolean
|
||||
get() = !getUsername().isEmpty() &&
|
||||
!getPassword().isEmpty() &&
|
||||
checkCookies(URI(BASE_URL)) &&
|
||||
checkCookies() &&
|
||||
!getCSRF().isEmpty()
|
||||
|
||||
private fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
||||
|
||||
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
||||
|
||||
private fun checkCookies(uri: URI): Boolean {
|
||||
private fun checkCookies(): Boolean {
|
||||
var ckCount = 0
|
||||
|
||||
for (ck in networkService.cookies.get(uri)) {
|
||||
val url = HttpUrl.parse(BASE_URL)!!
|
||||
for (ck in networkService.cookieManager.get(url)) {
|
||||
if (ck.name() == USER_SESSION_COOKIE || ck.name() == LOGGED_IN_COOKIE)
|
||||
ckCount++
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val created_at: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?) {
|
||||
|
||||
// Access token lives 1 day
|
||||
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||
}
|
||||
|
@ -0,0 +1,138 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Shikomori(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return IntRange(0, 10).map(Int::toString)
|
||||
}
|
||||
|
||||
override fun displayScore(track: Track): String {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track, getUsername())
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
return api.updateLibManga(track, getUsername())
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
.map { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLANNING = 5
|
||||
const val REPEATING = 6
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
override val name = "Shikomori"
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
private val interceptor by lazy { ShikomoriInterceptor(this, gson) }
|
||||
|
||||
private val api by lazy { ShikomoriApi(client, interceptor) }
|
||||
|
||||
override fun getLogo() = R.drawable.shikomori
|
||||
|
||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||
|
||||
override fun getStatusList(): List<Int> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
|
||||
}
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
when (status) {
|
||||
READING -> getString(R.string.reading)
|
||||
COMPLETED -> getString(R.string.completed)
|
||||
ON_HOLD -> getString(R.string.on_hold)
|
||||
DROPPED -> getString(R.string.dropped)
|
||||
PLANNING -> getString(R.string.plan_to_read)
|
||||
REPEATING -> getString(R.string.repeating)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(code: String): Completable {
|
||||
return api.accessToken(code).map { oauth: OAuth? ->
|
||||
interceptor.newAuth(oauth)
|
||||
if (oauth != null) {
|
||||
val user = api.getCurrentUser()
|
||||
saveCredentials(user.toString(), oauth.access_token)
|
||||
}
|
||||
}.doOnError {
|
||||
logout()
|
||||
}.toCompletable()
|
||||
}
|
||||
|
||||
fun saveToken(oauth: OAuth?) {
|
||||
val json = gson.toJson(oauth)
|
||||
preferences.trackToken(this).set(json)
|
||||
}
|
||||
|
||||
fun restoreToken(): OAuth? {
|
||||
return try {
|
||||
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).set(null)
|
||||
interceptor.newAuth(null)
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.jsonObject
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import okhttp3.*
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInterceptor) {
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
private val parser = JsonParser()
|
||||
private val jsonime = MediaType.parse("application/json; charset=utf-8")
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track, user_id: String): Observable<Track> {
|
||||
val payload = jsonObject(
|
||||
"user_rate" to jsonObject(
|
||||
"user_id" to user_id,
|
||||
"target_id" to track.media_id,
|
||||
"target_type" to "Manga",
|
||||
"chapters" to track.last_chapter_read,
|
||||
"score" to track.score.toInt(),
|
||||
"status" to track.toShikomoriStatus()
|
||||
)
|
||||
)
|
||||
val body = RequestBody.create(jsonime, payload.toString())
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/v2/user_rates")
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
.appendQueryParameter("order", "popularity")
|
||||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).array
|
||||
response.map { jsonToSearch(it.obj) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
return TrackSearch.create(TrackManager.SHIKOMORI).apply {
|
||||
media_id = obj["id"].asInt
|
||||
title = obj["name"].asString
|
||||
total_chapters = obj["chapters"].asInt
|
||||
cover_url = baseUrl + obj["image"].obj["preview"].asString
|
||||
summary = ""
|
||||
tracking_url = baseUrl + obj["url"].asString
|
||||
publishing_status = obj["status"].asString
|
||||
publishing_type = obj["kind"].asString
|
||||
start_date = obj.get("aired_on").nullString.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToTrack(obj: JsonObject): Track {
|
||||
return Track.create(TrackManager.SHIKOMORI).apply {
|
||||
media_id = obj["id"].asInt
|
||||
title = ""
|
||||
last_chapter_read = obj["chapters"].asInt
|
||||
total_chapters = obj["chapters"].asInt
|
||||
score = (obj["score"].asInt).toFloat()
|
||||
status = toTrackStatus(obj["status"].asString)
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
||||
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
||||
.appendQueryParameter("user_id", user_id)
|
||||
.appendQueryParameter("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).array
|
||||
if (response.size() > 1) {
|
||||
throw Exception("Too much mangas in response")
|
||||
}
|
||||
val entry = response.map {
|
||||
jsonToTrack(it.obj)
|
||||
}
|
||||
entry.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Int {
|
||||
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body()?.string()
|
||||
return parser.parse(user).obj["id"].asInt
|
||||
}
|
||||
|
||||
fun accessToken(code: String): Observable<OAuth> {
|
||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
gson.fromJson(responseBody, OAuth::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
companion object {
|
||||
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||
|
||||
private const val baseUrl = "https://shikimori.org"
|
||||
private const val apiUrl = "https://shikimori.org/api"
|
||||
private const val oauthUrl = "https://shikimori.org/oauth/token"
|
||||
private const val loginUrl = "https://shikimori.org/oauth/authorize"
|
||||
|
||||
private const val redirectUrl = "tachiyomi://shikimori-auth"
|
||||
private const val baseMangaUrl = "$apiUrl/mangas"
|
||||
|
||||
fun mangaUrl(remoteId: Int): String {
|
||||
return "$baseMangaUrl/$remoteId"
|
||||
}
|
||||
|
||||
fun authUrl() =
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build())
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*/
|
||||
private var oauth: OAuth? = shikomori.restoreToken()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val currAuth = oauth ?: throw Exception("Not authenticated with Shikomori")
|
||||
|
||||
val refreshToken = currAuth.refresh_token!!
|
||||
|
||||
// Refresh access token if expired.
|
||||
if (currAuth.isExpired()) {
|
||||
val response = chain.proceed(ShikomoriApi.refreshTokenRequest(refreshToken))
|
||||
if (response.isSuccessful) {
|
||||
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||
} else {
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
// Add the authorization header to the original request.
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.header("User-Agent", "Tachiyomi")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
fun newAuth(oauth: OAuth?) {
|
||||
this.oauth = oauth
|
||||
shikomori.saveToken(oauth)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
fun Track.toShikomoriStatus() = when (status) {
|
||||
Shikomori.READING -> "watching"
|
||||
Shikomori.COMPLETED -> "completed"
|
||||
Shikomori.ON_HOLD -> "on_hold"
|
||||
Shikomori.DROPPED -> "dropped"
|
||||
Shikomori.PLANNING -> "planned"
|
||||
Shikomori.REPEATING -> "rewatching"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
fun toTrackStatus(status: String) = when (status) {
|
||||
"watching" -> Shikomori.READING
|
||||
"completed" -> Shikomori.COMPLETED
|
||||
"on_hold" -> Shikomori.ON_HOLD
|
||||
"dropped" -> Shikomori.DROPPED
|
||||
"planned" -> Shikomori.PLANNING
|
||||
"rewatching" -> Shikomori.REPEATING
|
||||
|
||||
else -> throw Exception("Unknown status")
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.CookieSyncManager
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class AndroidCookieJar(context: Context) : CookieJar {
|
||||
|
||||
private val manager = CookieManager.getInstance()
|
||||
|
||||
private val syncManager by lazy { CookieSyncManager.createInstance(context) }
|
||||
|
||||
init {
|
||||
// Init sync manager when using anything below L
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
syncManager
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
|
||||
val urlString = url.toString()
|
||||
|
||||
for (cookie in cookies) {
|
||||
manager.setCookie(urlString, cookie.toString())
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
syncManager.sync()
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return get(url)
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl): List<Cookie> {
|
||||
val cookies = manager.getCookie(url.toString())
|
||||
|
||||
return if (cookies != null && !cookies.isEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(url: HttpUrl) {
|
||||
val cookies = manager.getCookie(url.toString()) ?: return
|
||||
val domain = ".${url.host()}"
|
||||
cookies.split(";")
|
||||
.map { it.substringBefore("=") }
|
||||
.onEach { manager.setCookie(domain, "$it=;Max-Age=-1") }
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
syncManager.sync()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
manager.removeAllCookies {}
|
||||
} else {
|
||||
manager.removeAllCookie()
|
||||
syncManager.sync()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,41 +1,52 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import com.squareup.duktape.Duktape
|
||||
import okhttp3.*
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import eu.kanade.tachiyomi.util.WebViewClientCompat
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class CloudflareInterceptor : Interceptor {
|
||||
|
||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||
|
||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||
|
||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||
|
||||
private val sPattern = Regex("""name="s" value="([^"]+)""")
|
||||
|
||||
private val kPattern = Regex("""k\s+=\s+'([^']+)';""")
|
||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
|
||||
private interface IBase64 {
|
||||
fun decode(input: String): String
|
||||
}
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val b64: IBase64 = object : IBase64 {
|
||||
override fun decode(input: String): String {
|
||||
return okio.ByteString.decodeBase64(input)!!.utf8()
|
||||
/**
|
||||
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
||||
* blocking the main thread too much. If used too often we could consider moving it to the
|
||||
* Application class.
|
||||
*/
|
||||
private val initWebView by lazy {
|
||||
if (Build.VERSION.SDK_INT >= 17) {
|
||||
WebSettings.getDefaultUserAgent(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
initWebView
|
||||
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
if (response.code() == 503 && response.header("Server") in serverCheck) {
|
||||
return try {
|
||||
chain.proceed(resolveChallenge(response))
|
||||
try {
|
||||
response.close()
|
||||
val solutionRequest = resolveWithWebView(chain.request())
|
||||
return chain.proceed(solutionRequest)
|
||||
} catch (e: Exception) {
|
||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||
// we don't crash the entire app
|
||||
@ -46,65 +57,98 @@ class CloudflareInterceptor : Interceptor {
|
||||
return response
|
||||
}
|
||||
|
||||
private fun resolveChallenge(response: Response): Request {
|
||||
Duktape.create().use { duktape ->
|
||||
val originalRequest = response.request()
|
||||
val url = originalRequest.url()
|
||||
val domain = url.host()
|
||||
val content = response.body()!!.string()
|
||||
private fun isChallengeSolutionUrl(url: String): Boolean {
|
||||
return "chk_jschl" in url
|
||||
}
|
||||
|
||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
||||
Thread.sleep(4000)
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun resolveWithWebView(request: Request): Request {
|
||||
// We need to lock this thread until the WebView finds the challenge solution url, because
|
||||
// OkHttp doesn't support asynchronous interceptors.
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
val operation = operationPattern.find(content)?.groups?.get(1)?.value
|
||||
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
|
||||
val pass = passPattern.find(content)?.groups?.get(1)?.value
|
||||
val s = sPattern.find(content)?.groups?.get(1)?.value
|
||||
var webView: WebView? = null
|
||||
var solutionUrl: String? = null
|
||||
var challengeFound = false
|
||||
|
||||
// If `k` is null, it uses old methods.
|
||||
val k = kPattern.find(content)?.groups?.get(1)?.value ?: ""
|
||||
val innerHTMLValue = Regex("""<div(.*)id="$k"(.*)>(.*)</div>""")
|
||||
.find(content)?.groups?.get(3)?.value ?: ""
|
||||
val origRequestUrl = request.url().toString()
|
||||
val headers = request.headers().toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||
|
||||
if (operation == null || challenge == null || pass == null || s == null) {
|
||||
throw Exception("Failed resolving Cloudflare challenge")
|
||||
handler.post {
|
||||
val view = WebView(context)
|
||||
webView = view
|
||||
view.settings.javaScriptEnabled = true
|
||||
view.settings.userAgentString = request.header("User-Agent")
|
||||
view.webViewClient = object : WebViewClientCompat() {
|
||||
|
||||
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||
if (isChallengeSolutionUrl(url)) {
|
||||
solutionUrl = url
|
||||
latch.countDown()
|
||||
}
|
||||
return solutionUrl != null
|
||||
}
|
||||
|
||||
override fun shouldInterceptRequestCompat(
|
||||
view: WebView,
|
||||
url: String
|
||||
): WebResourceResponse? {
|
||||
if (solutionUrl != null) {
|
||||
// Intercept any request when we have the solution.
|
||||
return WebResourceResponse("text/plain", "UTF-8", null)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
// Http error codes are only received since M
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
url == origRequestUrl && !challengeFound
|
||||
) {
|
||||
// The first request didn't return the challenge, abort.
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean
|
||||
) {
|
||||
if (isMainFrame) {
|
||||
if (errorCode == 503) {
|
||||
// Found the cloudflare challenge page.
|
||||
challengeFound = true
|
||||
} else {
|
||||
// Unlock thread, the challenge wasn't found.
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export native Base64 decode function to js object.
|
||||
duktape.set("b64", IBase64::class.java, b64)
|
||||
|
||||
// Return simulated innerHTML when call DOM.
|
||||
val simulatedDocumentJS = """var document = { getElementById: function (x) { return { innerHTML: "$innerHTMLValue" }; } }"""
|
||||
|
||||
val js = operation
|
||||
.replace(Regex("""a\.value = (.+\.toFixed\(10\);).+"""), "$1")
|
||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||
.replace("t.length", "${domain.length}")
|
||||
.replace("\n", "")
|
||||
|
||||
val result = duktape.evaluate("""$simulatedDocumentJS;$ATOB_JS;var t="$domain";$js""") as String
|
||||
|
||||
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("jschl_vc", challenge)
|
||||
.addQueryParameter("pass", pass)
|
||||
.addQueryParameter("s", s)
|
||||
.addQueryParameter("jschl_answer", result)
|
||||
.toString()
|
||||
|
||||
val cloudflareHeaders = originalRequest.headers()
|
||||
.newBuilder()
|
||||
.add("Referer", url.toString())
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml")
|
||||
.add("Accept-Language", "en")
|
||||
.build()
|
||||
|
||||
return GET(cloudflareUrl, cloudflareHeaders, cache = CacheControl.Builder().build())
|
||||
webView?.loadUrl(origRequestUrl, headers)
|
||||
}
|
||||
|
||||
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
|
||||
// around 4 seconds but it can take more due to slow networks or server issues.
|
||||
latch.await(12, TimeUnit.SECONDS)
|
||||
|
||||
handler.post {
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
}
|
||||
|
||||
val solution = solutionUrl ?: throw Exception("Challenge not found")
|
||||
|
||||
return Request.Builder().get()
|
||||
.url(solution)
|
||||
.headers(request.headers())
|
||||
.addHeader("Referer", origRequestUrl)
|
||||
.addHeader("Accept", "text/html,application/xhtml+xml,application/xml")
|
||||
.addHeader("Accept-Language", "en")
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
// atob() is browser API, Using Android's own function. (java.util.Base64 can't be used because of min API level)
|
||||
private const val ATOB_JS = """var atob = function (input) { return b64.decode(input) }"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,7 @@ package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import okhttp3.Cache
|
||||
import okhttp3.CipherSuite
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.TlsVersion
|
||||
import okhttp3.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
@ -15,11 +11,7 @@ import java.net.UnknownHostException
|
||||
import java.security.KeyManagementException
|
||||
import java.security.KeyStore
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import javax.net.ssl.*
|
||||
|
||||
class NetworkHelper(context: Context) {
|
||||
|
||||
@ -27,7 +19,7 @@ class NetworkHelper(context: Context) {
|
||||
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
private val cookieManager = PersistentCookieJar(context)
|
||||
val cookieManager = AndroidCookieJar(context)
|
||||
|
||||
val client = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
@ -36,12 +28,9 @@ class NetworkHelper(context: Context) {
|
||||
.build()
|
||||
|
||||
val cloudflareClient = client.newBuilder()
|
||||
.addInterceptor(CloudflareInterceptor())
|
||||
.addInterceptor(CloudflareInterceptor(context))
|
||||
.build()
|
||||
|
||||
val cookies: PersistentCookieStore
|
||||
get() = cookieManager.store
|
||||
|
||||
private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
|
||||
return this
|
||||
|
@ -1,19 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class PersistentCookieJar(context: Context) : CookieJar {
|
||||
|
||||
val store = PersistentCookieStore(context)
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
store.addAll(url, cookies)
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return store.get(url)
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import java.net.URI
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class PersistentCookieStore(context: Context) {
|
||||
|
||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||
|
||||
init {
|
||||
for ((key, value) in prefs.all) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val cookies = value as? Set<String>
|
||||
if (cookies != null) {
|
||||
try {
|
||||
val url = HttpUrl.parse("http://$key") ?: continue
|
||||
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap.put(key, nonExpiredCookies)
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val key = url.uri().host
|
||||
|
||||
// Append or replace the cookies for this domain.
|
||||
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
||||
for (cookie in cookies) {
|
||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
|
||||
if (pos == -1) {
|
||||
cookiesForDomain.add(cookie)
|
||||
} else {
|
||||
cookiesForDomain[pos] = cookie
|
||||
}
|
||||
}
|
||||
cookieMap.put(key, cookiesForDomain)
|
||||
|
||||
// Get cookies to be stored in disk
|
||||
val newValues = cookiesForDomain.asSequence()
|
||||
.filter { it.persistent() && !it.hasExpired() }
|
||||
.map(Cookie::toString)
|
||||
.toSet()
|
||||
|
||||
prefs.edit().putStringSet(key, newValues).apply()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeAll() {
|
||||
prefs.edit().clear().apply()
|
||||
cookieMap.clear()
|
||||
}
|
||||
|
||||
fun remove(uri: URI) {
|
||||
prefs.edit().remove(uri.host).apply()
|
||||
cookieMap.remove(uri.host)
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl) = get(url.uri().host)
|
||||
|
||||
fun get(uri: URI) = get(uri.host)
|
||||
|
||||
private fun get(url: String): List<Cookie> {
|
||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
}
|
||||
|
||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
||||
|
||||
}
|
@ -12,11 +12,7 @@ import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||
import eu.kanade.tachiyomi.source.online.english.*
|
||||
import eu.kanade.tachiyomi.source.online.german.WieManga
|
||||
import eu.kanade.tachiyomi.source.online.russian.Mangachan
|
||||
import eu.kanade.tachiyomi.source.online.russian.Mintmanga
|
||||
import eu.kanade.tachiyomi.source.online.russian.Readmanga
|
||||
import eu.kanade.tachiyomi.source.online.english.HentaiCafe
|
||||
import rx.Observable
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
@ -89,17 +85,7 @@ open class SourceManager(private val context: Context) {
|
||||
}
|
||||
|
||||
private fun createInternalSources(): List<Source> = listOf(
|
||||
LocalSource(context),
|
||||
Batoto(),
|
||||
Mangahere(),
|
||||
Mangafox(),
|
||||
Kissmanga(),
|
||||
Readmanga(),
|
||||
Mintmanga(),
|
||||
Mangachan(),
|
||||
Readmangatoday(),
|
||||
Mangasee(),
|
||||
WieManga()
|
||||
LocalSource(context)
|
||||
)
|
||||
|
||||
private fun createEHSources(): List<Source> {
|
||||
|
@ -1,31 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
class Batoto : Source {
|
||||
|
||||
override val id: Long = 1
|
||||
|
||||
override val name = "Batoto"
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return Observable.error(Exception("RIP Batoto"))
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.error(Exception("RIP Batoto"))
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return Observable.error(Exception("RIP Batoto"))
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$name (EN)"
|
||||
}
|
||||
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import com.squareup.duktape.Duktape
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Kissmanga : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 4
|
||||
|
||||
override val name = "Kissmanga"
|
||||
|
||||
override val baseUrl = "http://kissmanga.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder(): Headers.Builder {
|
||||
return Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) Gecko/20100101 Firefox/60")
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/MangaList/MostPopular?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("http://kissmanga.com/MangaList/LatestUpdate?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("td a:eq(0)").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
val title = it.text()
|
||||
//check if cloudfire email obfuscation is affecting title name
|
||||
if (title.contains("[email protected]", true)) {
|
||||
try {
|
||||
var str: String = it.html()
|
||||
//get the number
|
||||
str = str.substringAfter("data-cfemail=\"")
|
||||
str = str.substringBefore("\">[email")
|
||||
val sb = StringBuilder()
|
||||
//convert number to char
|
||||
val r = Integer.valueOf(str.substring(0, 2), 16)!!
|
||||
var i = 2
|
||||
while (i < str.length) {
|
||||
val c = (Integer.valueOf(str.substring(i, i + 2), 16) xor r).toChar()
|
||||
sb.append(c)
|
||||
i += 2
|
||||
}
|
||||
//replace the new word into the title
|
||||
manga.title = title.replace("[email protected]", sb.toString(), true)
|
||||
} catch (e: Exception) {
|
||||
//on error just default to obfuscated title
|
||||
Timber.e("error parsing [email protected]", e)
|
||||
manga.title = title
|
||||
}
|
||||
} else {
|
||||
manga.title = title
|
||||
}
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val form = FormBody.Builder().apply {
|
||||
add("mangaName", query)
|
||||
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Author -> add("authorArtist", filter.state)
|
||||
is Status -> add("status", arrayOf("", "Completed", "Ongoing")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre -> add("genres", genre.state.toString()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST("$baseUrl/AdvanceSearch", headers, form.build())
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.barContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
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.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.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("MM/dd/yyyy").parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val body = response.body()!!.string()
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
// Kissmanga now encrypts the urls, so we need to execute these two scripts in JS.
|
||||
val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body()!!.string()
|
||||
val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body()!!.string()
|
||||
|
||||
Duktape.create().use {
|
||||
it.evaluate(ca)
|
||||
it.evaluate(lo)
|
||||
|
||||
// There are two functions in an inline script needed to decrypt the urls. We find and
|
||||
// execute them.
|
||||
var p = Pattern.compile("(var.*CryptoJS.*)")
|
||||
var m = p.matcher(body)
|
||||
while (m.find()) {
|
||||
it.evaluate(m.group(1))
|
||||
}
|
||||
|
||||
// Finally find all the urls and decrypt them in JS.
|
||||
p = Pattern.compile("""lstImages.push\((.*)\);""")
|
||||
m = p.matcher(body)
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
val url = it.evaluate(m.group(1)) as String
|
||||
pages.add(Page(i++, "", url))
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page) = GET(page.url)
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Author : Filter.Text("Author")
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Author(),
|
||||
Status(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
|
||||
// on http://kissmanga.com/AdvanceSearch
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("4-Koma"),
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Comic"),
|
||||
Genre("Cooking"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Manga"),
|
||||
Genre("Manhua"),
|
||||
Genre("Manhwa"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Medical"),
|
||||
Genre("Music"),
|
||||
Genre("Mystery"),
|
||||
Genre("One shot"),
|
||||
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("Webtoon"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class Mangafox : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 3
|
||||
|
||||
override val name = "Mangafox"
|
||||
|
||||
override val baseUrl = "http://mangafox.la"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val pageStr = if (page != 1) "$page.htm" else ""
|
||||
return GET("$baseUrl/directory/$pageStr", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val pageStr = if (page != 1) "$page.htm" else ""
|
||||
return GET("$baseUrl/directory/$pageStr?latest")
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.title").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is Type -> url.addQueryParameter("type", if (filter.state == 0) "" else filter.state.toString())
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||
}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.title").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#title").first()
|
||||
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
|
||||
val sideInfoElement = document.select("#series_info").first()
|
||||
val licensedElement = document.select("div.warning").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = rowElement.select("td:eq(1)").first()?.text()
|
||||
manga.artist = rowElement.select("td:eq(2)").first()?.text()
|
||||
manga.genre = rowElement.select("td:eq(3)").first()?.text()
|
||||
manga.description = infoElement.select("p.summary").first()?.text()
|
||||
val isLicensed = licensedElement?.text()?.contains("licensed")
|
||||
if (isLicensed == true) {
|
||||
manga.status = SManga.LICENSED
|
||||
} else {
|
||||
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
}
|
||||
|
||||
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div#chapters li div"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a.tips").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = element.select("span.title.nowrap").first()?.text()?.let { urlElement.text() + " - " + it } ?: urlElement.text()
|
||||
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return if ("Today" in date || " ago" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else if ("Yesterday" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -1)
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val url = document.baseUri().substringBeforeLast('/')
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
|
||||
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
val url = document.getElementById("image").attr("src")
|
||||
return if ("compressed?token=" !in url) {
|
||||
url
|
||||
} else {
|
||||
"http://mangafox.me/media/logo.png"
|
||||
}
|
||||
}
|
||||
|
||||
private class Status(val id: String = "is_completed") : 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 Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
Type(),
|
||||
Status(),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
|
||||
// on http://mangafox.me/search.php
|
||||
private fun getGenreList() = listOf(
|
||||
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")
|
||||
)
|
||||
|
||||
}
|
@ -1,259 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class Mangahere : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 2
|
||||
|
||||
override val name = "Mangahere"
|
||||
|
||||
override val baseUrl = "http://www.mangahere.cc"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val trustManager = object : X509TrustManager {
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private val sslContext = SSLContext.getInstance("SSL").apply {
|
||||
init(null, arrayOf(trustManager), SecureRandom())
|
||||
}
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.sslSocketFactory(sslContext.socketFactory, trustManager)
|
||||
.build()
|
||||
|
||||
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/directory/$page.htm?views.za", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers)
|
||||
}
|
||||
|
||||
private fun mangaFromElement(query: String, element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select(query).first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("div.title > a", element)
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state])
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||
}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("a.manga_info", element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select(".manga_detail_top").first()
|
||||
val infoElement = detailElement.select(".detail_topText").first()
|
||||
val licensedElement = document.select(".mt10.color_ff00.mb10").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("a[href*=author/]").first()?.text()
|
||||
manga.artist = infoElement.select("a[href*=artist/]").first()?.text()
|
||||
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
|
||||
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
||||
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
|
||||
|
||||
if (licensedElement?.text()?.contains("licensed") == true) {
|
||||
manga.status = SManga.LICENSED
|
||||
} else {
|
||||
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val parentEl = element.select("span.left").first()
|
||||
|
||||
val urlElement = parentEl.select("a").first()
|
||||
|
||||
var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: ""
|
||||
if (volume.length > 0) {
|
||||
volume = " - " + volume
|
||||
}
|
||||
|
||||
var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: ""
|
||||
if (title.length > 0) {
|
||||
title = " - " + title
|
||||
}
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text() + volume + title
|
||||
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return if ("Today" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else if ("Yesterday" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -1)
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val licensedError = document.select(".mangaread_error > .mt10").first()
|
||||
if (licensedError != null) {
|
||||
throw Exception(licensedError.text())
|
||||
}
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||
if (!it.attr("value").contains("featured.html")) {
|
||||
pages.add(Page(pages.size, "http:" + it.attr("value")))
|
||||
}
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||
|
||||
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 Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
Type(),
|
||||
Status(),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...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
|
||||
private fun getGenreList() = listOf(
|
||||
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")
|
||||
)
|
||||
|
||||
}
|
@ -1,249 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Mangasee : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 9
|
||||
|
||||
override val name = "Mangasee"
|
||||
|
||||
override val baseUrl = "http://mangaseeonline.us"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?")
|
||||
|
||||
private val indexPattern = Pattern.compile("-index-(.*?)-")
|
||||
|
||||
private val catalogHeaders = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
add("Host", "mangaseeonline.us")
|
||||
}.build()
|
||||
|
||||
override fun popularMangaSelector() = "div.requested > div.row"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending")
|
||||
return POST(requestUrl, catalogHeaders, body.build())
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.resultLink").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun searchMangaSelector() = "div.requested > div.row"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search/request.php")!!.newBuilder()
|
||||
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
||||
val genres = mutableListOf<String>()
|
||||
val genresNo = mutableListOf<String>()
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Sort -> {
|
||||
if (filter.state?.index != 0)
|
||||
url.addQueryParameter("sortBy", if (filter.state?.index == 1) "dateUpdated" else "popularity")
|
||||
if (filter.state?.ascending != true)
|
||||
url.addQueryParameter("sortOrder", "descending")
|
||||
}
|
||||
is SelectField -> 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 GenreList -> filter.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
|
||||
Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (genres.isNotEmpty()) url.addQueryParameter("genre", genres.joinToString(","))
|
||||
if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(","))
|
||||
|
||||
val (body, requestUrl) = convertQueryToPost(page, url.toString())
|
||||
return POST(requestUrl, catalogHeaders, body.build())
|
||||
}
|
||||
|
||||
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
||||
val url = HttpUrl.parse(url)!!
|
||||
val body = FormBody.Builder().add("page", page.toString())
|
||||
for (i in 0..url.querySize() - 1) {
|
||||
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
||||
}
|
||||
val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath()
|
||||
return Pair(body, requestUrl)
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.resultLink").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select("div.well > div.row").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text()
|
||||
manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").map { it.text() }.joinToString()
|
||||
manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
|
||||
manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing (Scan)") -> SManga.ONGOING
|
||||
status.contains("Complete (Scan)") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapter-list > a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
|
||||
chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(dateAsString: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val fullUrl = document.baseUri()
|
||||
val url = fullUrl.substringBeforeLast('/')
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
val series = document.select("input.IndexName").first().attr("value")
|
||||
val chapter = document.select("span.CurChapter").first().text()
|
||||
var index = ""
|
||||
|
||||
val m = indexPattern.matcher(fullUrl)
|
||||
if (m.find()) {
|
||||
val indexNumber = m.group(1)
|
||||
index = "-index-$indexNumber"
|
||||
}
|
||||
|
||||
document.select("div.ContainerNav").first().select("select.PageSelect > option").forEach {
|
||||
pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun latestUpdatesSelector(): String = "a.latestSeries"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "http://mangaseeonline.net/home/latest.request.php"
|
||||
val (body, requestUrl) = convertQueryToPost(page, url)
|
||||
return POST(requestUrl, catalogHeaders, body.build())
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.latestSeries").first().let {
|
||||
val chapterUrl = it.attr("href")
|
||||
val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
|
||||
val indexOfLastPath = chapterUrl.lastIndexOf("/")
|
||||
val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl)
|
||||
val defaultText = it.select("p.clamp2").text()
|
||||
val m = recentUpdatesPattern.matcher(defaultText)
|
||||
val title = if (m.matches()) m.group(1) else defaultText
|
||||
manga.setUrlWithoutDomain("/manga" + mangaUrl)
|
||||
manga.title = title
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false))
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class SelectField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Years", "year"),
|
||||
TextField("Author", "author"),
|
||||
SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
|
||||
SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
|
||||
SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
|
||||
Sort(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// http://mangasee.co/advanced-search/
|
||||
private fun getGenreList() = listOf(
|
||||
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")
|
||||
)
|
||||
|
||||
}
|
@ -1,224 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.*
|
||||
|
||||
class Readmangatoday : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 8
|
||||
|
||||
override val name = "ReadMangaToday"
|
||||
|
||||
override val baseUrl = "https://www.readmng.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient get() = network.cloudflareClient
|
||||
|
||||
/**
|
||||
* Search only returns data with this set
|
||||
*/
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/hot-manga/$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/latest-releases/$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > div.box"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("div.title > h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val builder = okhttp3.FormBody.Builder()
|
||||
builder.add("manga-name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
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 GenreList -> filter.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> builder.add("include[]", genre.id.toString())
|
||||
Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", genre.id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST("$baseUrl/service/advanced_search", headers, builder.build())
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.style-list > div.box"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("div.title > h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select("div.movie-meta").first()
|
||||
val genreElement = detailElement.select("dl.dl-horizontal > dd:eq(5) a")
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = document.select("ul.cast-list li.director > ul a").first()?.text()
|
||||
manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text()
|
||||
manga.description = detailElement.select("li.movie-detail").first()?.text()
|
||||
manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
|
||||
|
||||
var genres = mutableListOf<String>()
|
||||
genreElement?.forEach { genres.add(it.text()) }
|
||||
manga.genre = genres.joinToString(", ")
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul.chp_lst > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.select("span.val").text()
|
||||
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
val dateWords: List<String> = date.split(" ")
|
||||
|
||||
if (dateWords.size == 3) {
|
||||
val timeAgo = Integer.parseInt(dateWords[0])
|
||||
val date: Calendar = Calendar.getInstance()
|
||||
|
||||
if (dateWords[1].contains("Minute")) {
|
||||
date.add(Calendar.MINUTE, -timeAgo)
|
||||
} else if (dateWords[1].contains("Hour")) {
|
||||
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")) {
|
||||
date.add(Calendar.WEEK_OF_YEAR, -timeAgo)
|
||||
} else if (dateWords[1].contains("Month")) {
|
||||
date.add(Calendar.MONTH, -timeAgo)
|
||||
} else if (dateWords[1].contains("Year")) {
|
||||
date.add(Calendar.YEAR, -timeAgo)
|
||||
}
|
||||
|
||||
return date.timeInMillis
|
||||
}
|
||||
|
||||
return 0L
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("#chapter_img").first().attr("src")
|
||||
|
||||
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.Select<String>("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author-name"),
|
||||
TextField("Artist", "artist-name"),
|
||||
Type(),
|
||||
Status(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...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
|
||||
private fun getGenreList() = listOf(
|
||||
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)
|
||||
)
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.german
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class WieManga : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 10
|
||||
|
||||
override val name = "Wie Manga!"
|
||||
|
||||
override val baseUrl = "http://www.wiemanga.com"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = ".booklist td > div"
|
||||
|
||||
override fun latestUpdatesSelector() = ".booklist td > div"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list/Hot-Book/", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list/New-Update/", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val image = element.select("dt img")
|
||||
val title = element.select("dd a:first-child")
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain(title.attr("href"))
|
||||
manga.title = title.text()
|
||||
manga.thumbnail_url = image.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = null
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/search/?wd=$query", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".searchresult td > div"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val image = element.select(".resultimg img")
|
||||
val title = element.select(".resultbookname")
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain(title.attr("href"))
|
||||
manga.title = title.text()
|
||||
manga.thumbnail_url = image.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = ".pagetor a.l"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first()
|
||||
val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
|
||||
manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text()
|
||||
manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "")
|
||||
manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
|
||||
|
||||
if (manga.author == "RSS")
|
||||
manga.author = null
|
||||
|
||||
if (manga.artist == "RSS")
|
||||
manga.artist = null
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select(".col1 a").first()
|
||||
val dateElement = element.select(".col3 a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
document.select("select#page").first().select("option").forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
|
||||
|
||||
}
|
@ -1,290 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class Mangachan : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 7
|
||||
|
||||
override val name = "Mangachan"
|
||||
|
||||
override val baseUrl = "http://mangachan.me"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
var pageNum = 1
|
||||
when {
|
||||
page < 1 -> pageNum = 1
|
||||
page >= 1 -> pageNum = page
|
||||
}
|
||||
val url = if (query.isNotEmpty()) {
|
||||
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
|
||||
} else {
|
||||
|
||||
var genres = ""
|
||||
var order = ""
|
||||
var statusParam = true
|
||||
var status = ""
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is GenreList -> {
|
||||
filter.state.forEach { f ->
|
||||
if (!f.isIgnored()) {
|
||||
genres += (if (f.isExcluded()) "-" else "") + f.id + '+'
|
||||
}
|
||||
}
|
||||
}
|
||||
is OrderBy -> {
|
||||
if (filter.state!!.ascending && filter.state!!.index == 0) {
|
||||
statusParam = false
|
||||
}
|
||||
}
|
||||
is Status -> status = arrayOf("", "all_done", "end", "ongoing", "new_ch")[filter.state]
|
||||
}
|
||||
}
|
||||
|
||||
if (genres.isNotEmpty()) {
|
||||
for (filter in filters) {
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
order = if (filter.state!!.ascending) {
|
||||
arrayOf("", "&n=favasc", "&n=abcdesc", "&n=chasc")[filter.state!!.index]
|
||||
} else {
|
||||
arrayOf("&n=dateasc", "&n=favdesc", "&n=abcasc", "&n=chdesc")[filter.state!!.index]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (statusParam) {
|
||||
"$baseUrl/tags/${genres.dropLast(1)}$order?offset=${20 * (pageNum - 1)}&status=$status"
|
||||
} else {
|
||||
"$baseUrl/tags/$status/${genres.dropLast(1)}/$order?offset=${20 * (pageNum - 1)}"
|
||||
}
|
||||
} else {
|
||||
for (filter in filters) {
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
order = if (filter.state!!.ascending) {
|
||||
arrayOf("manga/new", "manga/new&n=favasc", "manga/new&n=abcdesc", "manga/new&n=chasc")[filter.state!!.index]
|
||||
} else {
|
||||
arrayOf("manga/new&n=dateasc", "mostfavorites", "catalog", "sortch")[filter.state!!.index]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (statusParam) {
|
||||
"$baseUrl/$order?offset=${20 * (pageNum - 1)}&status=$status"
|
||||
} else {
|
||||
"$baseUrl/$order/$status?offset=${20 * (pageNum - 1)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newestch?page=$page")
|
||||
|
||||
override fun popularMangaSelector() = "div.content_row"
|
||||
|
||||
override fun latestUpdatesSelector() = "ul.area_rightNews li"
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("div.manga_images img").first().attr("src")
|
||||
element.select("h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a:nth-child(1)").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a:contains(Далее)"
|
||||
|
||||
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
var hasNextPage = false
|
||||
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
val nextSearchPage = document.select(searchMangaNextPageSelector())
|
||||
if (nextSearchPage.isNotEmpty()) {
|
||||
val query = document.select("input#searchinput").first().attr("value")
|
||||
val pageNum = nextSearchPage.let { selector ->
|
||||
val onClick = selector.attr("onclick")
|
||||
onClick?.split("""\\d+""")
|
||||
}
|
||||
nextSearchPage.attr("href", "$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum")
|
||||
hasNextPage = true
|
||||
}
|
||||
|
||||
val nextGenresPage = document.select(searchGenresNextPageSelector())
|
||||
if (nextGenresPage.isNotEmpty()) {
|
||||
hasNextPage = true
|
||||
}
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("table.mangatitle").first()
|
||||
val descElement = document.select("div#description").first()
|
||||
val imgElement = document.select("img#cover").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
|
||||
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
|
||||
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
||||
manga.description = descElement.textNodes().first().text()
|
||||
manga.thumbnail_url = imgElement.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("перевод завершен") -> SManga.COMPLETED
|
||||
element.contains("перевод продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("div.date").first()?.text()?.let {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body()!!.string()
|
||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
||||
val endIndex = html.indexOf(",]", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
||||
val pageUrls = trimmedHtml.split(',')
|
||||
|
||||
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
|
||||
private class OrderBy : Filter.Sort("Сортировка",
|
||||
arrayOf("Дата", "Популярность", "Имя", "Главы"),
|
||||
Filter.Sort.Selection(1, false))
|
||||
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Status(),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
|
||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")]
|
||||
* .map(el => `Genre("${el.getAttribute('href').substr(6)}")`).join(',\n')
|
||||
* on http://mangachan.me/
|
||||
*/
|
||||
private fun getGenreList() = 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("ёнкома")
|
||||
)
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Mintmanga : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 6
|
||||
|
||||
override val name = "Mintmanga"
|
||||
|
||||
override val baseUrl = "http://mintmanga.com"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.tile"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.tile"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("img.lazy").first()?.attr("data-original")
|
||||
element.select("h3 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search/advanced")!!.newBuilder()
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
if (genre.state != Filter.TriState.STATE_IGNORE) {
|
||||
url.addQueryParameter(genre.id, arrayOf("=", "=in", "=ex")[genre.state])
|
||||
}
|
||||
}
|
||||
is Category -> filter.state.forEach { category ->
|
||||
if (category.state != Filter.TriState.STATE_IGNORE) {
|
||||
url.addQueryParameter(category.id, arrayOf("=", "=in", "=ex")[category.state])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!query.isEmpty()) {
|
||||
url.addQueryParameter("q", query)
|
||||
}
|
||||
return GET(url.toString().replace("=%3D", "="), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector(): Nothing? = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.leftContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("span.elem_author").first()?.text()
|
||||
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
|
||||
manga.description = infoElement.select("div.manga-description").text()
|
||||
manga.status = parseStatus(infoElement.html())
|
||||
manga.thumbnail_url = infoElement.select("img").attr("data-full")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
val urlText = urlElement.text()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mtr=1")
|
||||
if (urlText.endsWith(" новое")) {
|
||||
chapter.name = urlText.dropLast(6)
|
||||
} else {
|
||||
chapter.name = urlText
|
||||
}
|
||||
chapter.date_upload = element.select("td.hidden-xxs").last()?.text()?.let {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
val basic = Regex("""\s*([0-9]+)(\s-\s)([0-9]+)\s*""")
|
||||
val extra = Regex("""\s*([0-9]+\sЭкстра)\s*""")
|
||||
val single = Regex("""\s*Сингл\s*""")
|
||||
when {
|
||||
basic.containsMatchIn(chapter.name) -> {
|
||||
basic.find(chapter.name)?.let {
|
||||
val number = it.groups[3]?.value!!
|
||||
chapter.chapter_number = number.toFloat()
|
||||
}
|
||||
}
|
||||
extra.containsMatchIn(chapter.name) -> // Extra chapters doesn't contain chapter number
|
||||
chapter.chapter_number = -2f
|
||||
single.containsMatchIn(chapter.name) -> // Oneshoots, doujinshi and other mangas with one chapter
|
||||
chapter.chapter_number = 1f
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body()!!.string()
|
||||
val beginIndex = html.indexOf("rm_h.init( [")
|
||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||
|
||||
val p = Pattern.compile("'.*?','.*?',\".*?\"")
|
||||
val m = p.matcher(trimmedHtml)
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||
val url = if (urlParts[1].isEmpty() && urlParts[2].startsWith("/static/")) {
|
||||
baseUrl + urlParts[2]
|
||||
} else {
|
||||
urlParts[1] + urlParts[0] + urlParts[2]
|
||||
}
|
||||
pages.add(Page(i++, "", url))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeader = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
add("Referer", baseUrl)
|
||||
}.build()
|
||||
return GET(page.imageUrl!!, imgHeader)
|
||||
}
|
||||
|
||||
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
private class Category(categories: List<Genre>) : Filter.Group<Genre>("Category", categories)
|
||||
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")]
|
||||
* .map(el => `Genre("${el.textContent.trim()}", "${el.getAttribute('onclick')
|
||||
* .substr(31,el.getAttribute('onclick').length-33)"})`).join(',\n')
|
||||
* on http://mintmanga.com/search/advanced
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Category(getCategoryList()),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
private fun getCategoryList() = listOf(
|
||||
Genre("В цвете", "el_4614"),
|
||||
Genre("Веб", "el_1355"),
|
||||
Genre("Выпуск приостановлен", "el_5232"),
|
||||
Genre("Ёнкома", "el_2741"),
|
||||
Genre("Комикс западный", "el_1903"),
|
||||
Genre("Комикс русский", "el_2173"),
|
||||
Genre("Манхва", "el_1873"),
|
||||
Genre("Маньхуа", "el_1875"),
|
||||
Genre("Не Яой", "el_1874"),
|
||||
Genre("Ранобэ", "el_5688"),
|
||||
Genre("Сборник", "el_1348")
|
||||
)
|
||||
|
||||
private fun getGenreList() = 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_5676"),
|
||||
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")
|
||||
)
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Readmanga : ParsedHttpSource() {
|
||||
|
||||
override val id: Long = 5
|
||||
|
||||
override val name = "Readmanga"
|
||||
|
||||
override val baseUrl = "http://readmanga.me"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div.tile"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.tile"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("img.lazy").first()?.attr("data-original")
|
||||
element.select("h3 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search/advanced")!!.newBuilder()
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
if (genre.state != Filter.TriState.STATE_IGNORE) {
|
||||
url.addQueryParameter(genre.id, arrayOf("=", "=in", "=ex")[genre.state])
|
||||
}
|
||||
}
|
||||
is Category -> filter.state.forEach { category ->
|
||||
if (category.state != Filter.TriState.STATE_IGNORE) {
|
||||
url.addQueryParameter(category.id, arrayOf("=", "=in", "=ex")[category.state])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!query.isEmpty()) {
|
||||
url.addQueryParameter("q", query)
|
||||
}
|
||||
return GET(url.toString().replace("=%3D", "="), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector(): Nothing? = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.leftContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("span.elem_author").first()?.text()
|
||||
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
|
||||
manga.description = infoElement.select("div.manga-description").text()
|
||||
manga.status = parseStatus(infoElement.html())
|
||||
manga.thumbnail_url = infoElement.select("img").attr("data-full")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
val urlText = urlElement.text()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mtr=1")
|
||||
if (urlText.endsWith(" новое")) {
|
||||
chapter.name = urlText.dropLast(6)
|
||||
} else {
|
||||
chapter.name = urlText
|
||||
}
|
||||
chapter.date_upload = element.select("td.hidden-xxs").last()?.text()?.let {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
val basic = Regex("""\s*([0-9]+)(\s-\s)([0-9]+)\s*""")
|
||||
val extra = Regex("""\s*([0-9]+\sЭкстра)\s*""")
|
||||
val single = Regex("""\s*Сингл\s*""")
|
||||
when {
|
||||
basic.containsMatchIn(chapter.name) -> {
|
||||
basic.find(chapter.name)?.let {
|
||||
val number = it.groups[3]?.value!!
|
||||
chapter.chapter_number = number.toFloat()
|
||||
}
|
||||
}
|
||||
extra.containsMatchIn(chapter.name) -> // Extra chapters doesn't contain chapter number
|
||||
chapter.chapter_number = -2f
|
||||
single.containsMatchIn(chapter.name) -> // Oneshoots, doujinshi and other mangas with one chapter
|
||||
chapter.chapter_number = 1f
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body()!!.string()
|
||||
val beginIndex = html.indexOf("rm_h.init( [")
|
||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||
|
||||
val p = Pattern.compile("'.*?','.*?',\".*?\"")
|
||||
val m = p.matcher(trimmedHtml)
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||
val url = if (urlParts[1].isEmpty() && urlParts[2].startsWith("/static/")) {
|
||||
baseUrl + urlParts[2]
|
||||
} else {
|
||||
urlParts[1] + urlParts[0] + urlParts[2]
|
||||
}
|
||||
pages.add(Page(i++, "", url))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeader = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
add("Referer", baseUrl)
|
||||
}.build()
|
||||
return GET(page.imageUrl!!, imgHeader)
|
||||
}
|
||||
|
||||
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
private class Category(categories: List<Genre>) : Filter.Group<Genre>("Category", categories)
|
||||
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")]
|
||||
* .map(el => `Genre("${el.textContent.trim()}", $"{el.getAttribute('onclick')
|
||||
* .substr(31,el.getAttribute('onclick').length-33)"})`).join(',\n')
|
||||
* on http://readmanga.me/search/advanced
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Category(getCategoryList()),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
private fun getCategoryList() = listOf(
|
||||
Genre("В цвете", "el_7290"),
|
||||
Genre("Веб", "el_2160"),
|
||||
Genre("Выпуск приостановлен", "el_8033"),
|
||||
Genre("Ёнкома", "el_2161"),
|
||||
Genre("Комикс западный", "el_3515"),
|
||||
Genre("Манхва", "el_3001"),
|
||||
Genre("Маньхуа", "el_3002"),
|
||||
Genre("Ранобэ", "el_8575"),
|
||||
Genre("Сборник", "el_2157")
|
||||
)
|
||||
|
||||
private fun getGenreList() = 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")
|
||||
)
|
||||
}
|
@ -21,8 +21,10 @@ import uy.kohesive.injekt.injectLazy
|
||||
* This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter]
|
||||
* [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class CatalogueSearchController(protected val initialQuery: String? = null) :
|
||||
NucleusController<CatalogueSearchPresenter>(),
|
||||
open class CatalogueSearchController(
|
||||
protected val initialQuery: String? = null,
|
||||
protected val extensionFilter: String? = null
|
||||
) : NucleusController<CatalogueSearchPresenter>(),
|
||||
CatalogueSearchCardAdapter.OnMangaClickListener, CatalogueSearchAdapter.OnMoreClickListener {
|
||||
|
||||
/**
|
||||
@ -68,7 +70,7 @@ open class CatalogueSearchController(protected val initialQuery: String? = null)
|
||||
* @return instance of [CatalogueSearchPresenter]
|
||||
*/
|
||||
override fun createPresenter(): CatalogueSearchPresenter {
|
||||
return CatalogueSearchPresenter(initialQuery)
|
||||
return CatalogueSearchPresenter(initialQuery, extensionFilter)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -205,4 +207,4 @@ open class CatalogueSearchController(protected val initialQuery: String? = null)
|
||||
router.pushController(controller.withFadeTransaction())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
@ -21,6 +22,7 @@ import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Presenter of [CatalogueSearchController]
|
||||
@ -32,6 +34,7 @@ import uy.kohesive.injekt.api.get
|
||||
*/
|
||||
open class CatalogueSearchPresenter(
|
||||
val initialQuery: String? = "",
|
||||
val initialExtensionFilter: String? = null,
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val preferencesHelper: PreferencesHelper = Injekt.get()
|
||||
@ -40,7 +43,7 @@ open class CatalogueSearchPresenter(
|
||||
/**
|
||||
* Enabled sources.
|
||||
*/
|
||||
val sources by lazy { getEnabledSources() }
|
||||
val sources by lazy { getSourcesToQuery() }
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
@ -63,9 +66,16 @@ open class CatalogueSearchPresenter(
|
||||
*/
|
||||
private var fetchImageSubscription: Subscription? = null
|
||||
|
||||
private val extensionManager by injectLazy<ExtensionManager>()
|
||||
|
||||
private var extensionFilter: String? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
extensionFilter = savedState?.getString(CatalogueSearchPresenter::extensionFilter.name) ?:
|
||||
initialExtensionFilter
|
||||
|
||||
// Perform a search with previous or initial state
|
||||
search(savedState?.getString(BrowseCataloguePresenter::query.name) ?: initialQuery.orEmpty())
|
||||
}
|
||||
@ -78,6 +88,7 @@ open class CatalogueSearchPresenter(
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
state.putString(BrowseCataloguePresenter::query.name, query)
|
||||
state.putString(CatalogueSearchPresenter::extensionFilter.name, extensionFilter)
|
||||
super.onSave(state)
|
||||
}
|
||||
|
||||
@ -97,6 +108,26 @@ open class CatalogueSearchPresenter(
|
||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||
}
|
||||
|
||||
private fun getSourcesToQuery(): List<CatalogueSource> {
|
||||
val filter = extensionFilter
|
||||
val enabledSources = getEnabledSources()
|
||||
if (filter.isNullOrEmpty()) {
|
||||
return enabledSources
|
||||
}
|
||||
|
||||
val filterSources = extensionManager.installedExtensions
|
||||
.filter { it.pkgName == filter }
|
||||
.flatMap { it.sources }
|
||||
.filter { it in enabledSources }
|
||||
.filterIsInstance<CatalogueSource>()
|
||||
|
||||
if (filterSources.isEmpty()) {
|
||||
return enabledSources
|
||||
}
|
||||
|
||||
return filterSources
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a catalogue search item
|
||||
*/
|
||||
|
@ -2,8 +2,9 @@ package eu.kanade.tachiyomi.ui.main
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.ActivityManager
|
||||
import android.app.Service
|
||||
import android.app.SearchManager
|
||||
import android.app.usage.UsageStatsManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
@ -21,6 +22,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||
import eu.kanade.tachiyomi.ui.extension.ExtensionController
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||
@ -218,6 +220,29 @@ class MainActivity : BaseActivity() {
|
||||
setSelectedDrawerItem(R.id.nav_drawer_downloads)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEARCH, "com.google.android.gms.actions.SEARCH_ACTION" -> {
|
||||
//If the intent match the "standard" Android search intent
|
||||
// or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant)
|
||||
|
||||
//Get the search query provided in extras, and if not null, perform a global search with it.
|
||||
val query = intent.getStringExtra(SearchManager.QUERY)
|
||||
if (query != null && !query.isEmpty()) {
|
||||
if (router.backstackSize > 1) {
|
||||
router.popToRoot()
|
||||
}
|
||||
router.pushController(CatalogueSearchController(query).withFadeTransaction())
|
||||
}
|
||||
}
|
||||
INTENT_SEARCH -> {
|
||||
val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
|
||||
val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
|
||||
if (query != null && !query.isEmpty()) {
|
||||
if (router.backstackSize > 1) {
|
||||
router.popToRoot()
|
||||
}
|
||||
router.pushController(CatalogueSearchController(query, filter).withFadeTransaction())
|
||||
}
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
@ -393,6 +418,10 @@ class MainActivity : BaseActivity() {
|
||||
const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||
const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
|
||||
const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
|
||||
|
||||
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
|
||||
const val INTENT_SEARCH_QUERY = "query"
|
||||
const val INTENT_SEARCH_FILTER = "filter"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -159,6 +159,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_open_in_browser -> openInBrowser()
|
||||
R.id.action_open_in_web_view -> openInWebView()
|
||||
R.id.action_share -> shareManga()
|
||||
R.id.action_add_to_home_screen -> addToHomeScreen()
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
@ -332,6 +333,19 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
}
|
||||
}
|
||||
|
||||
private fun openInWebView() {
|
||||
val source = presenter.source as? HttpSource ?: return
|
||||
|
||||
val url = try {
|
||||
source.mangaDetailsRequest(presenter.manga).url().toString()
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
}
|
||||
|
||||
parentController?.router?.pushController(MangaWebViewController(source.id, url)
|
||||
.withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
|
||||
*/
|
||||
|
@ -0,0 +1,58 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
import eu.kanade.tachiyomi.util.WebViewClientCompat
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaWebViewController(bundle: Bundle? = null) : BaseController(bundle) {
|
||||
|
||||
private val sourceManager by injectLazy<SourceManager>()
|
||||
|
||||
constructor(sourceId: Long, url: String) : this(Bundle().apply {
|
||||
putLong(SOURCE_KEY, sourceId)
|
||||
putString(URL_KEY, url)
|
||||
})
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.manga_info_web_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
val source = sourceManager.get(args.getLong(SOURCE_KEY)) as? HttpSource ?: return
|
||||
val url = args.getString(URL_KEY) ?: return
|
||||
val headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||
|
||||
val web = view as WebView
|
||||
web.webViewClient = object : WebViewClientCompat() {
|
||||
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||
view.loadUrl(url)
|
||||
return true
|
||||
}
|
||||
}
|
||||
web.settings.javaScriptEnabled = true
|
||||
web.settings.userAgentString = source.headers["User-Agent"]
|
||||
web.loadUrl(url, headers)
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
val web = view as WebView
|
||||
web.stopLoading()
|
||||
web.destroy()
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SOURCE_KEY = "source_key"
|
||||
const val URL_KEY = "url_key"
|
||||
}
|
||||
|
||||
}
|
@ -88,12 +88,25 @@ class ReaderPresenter(
|
||||
private val chapterList by lazy {
|
||||
val manga = manga!!
|
||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||
|
||||
val selectedChapter = dbChapters.find { it.id == chapterId }
|
||||
?: error("Requested chapter of id $chapterId not found in chapter list")
|
||||
?: error("Requested chapter of id $chapterId not found in chapter list")
|
||||
|
||||
val chaptersForReader =
|
||||
if (preferences.skipRead()) {
|
||||
var list = dbChapters.filter { it -> !it.read }.toMutableList()
|
||||
val find = list.find { it.id == chapterId }
|
||||
if (find == null) {
|
||||
list.add(selectedChapter)
|
||||
}
|
||||
list
|
||||
} else {
|
||||
dbChapters
|
||||
}
|
||||
|
||||
when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> ChapterLoadBySource().get(dbChapters)
|
||||
Manga.SORTING_NUMBER -> ChapterLoadByNumber().get(dbChapters, selectedChapter)
|
||||
Manga.SORTING_SOURCE -> ChapterLoadBySource().get(chaptersForReader)
|
||||
Manga.SORTING_NUMBER -> ChapterLoadByNumber().get(chaptersForReader, selectedChapter)
|
||||
else -> error("Unknown sorting method")
|
||||
}.map(::ReaderChapter)
|
||||
}
|
||||
@ -169,12 +182,12 @@ class ReaderPresenter(
|
||||
if (!needsInit()) return
|
||||
|
||||
db.getManga(mangaId).asRxObservable()
|
||||
.first()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { init(it, initialChapterId) }
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Ignore onNext event
|
||||
}, ReaderActivity::setInitialChapterError)
|
||||
.first()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { init(it, initialChapterId) }
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Ignore onNext event
|
||||
}, ReaderActivity::setInitialChapterError)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -197,13 +210,13 @@ class ReaderPresenter(
|
||||
// Read chapterList from an io thread because it's retrieved lazily and would block main.
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
activeChapterSubscription = Observable
|
||||
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
||||
.flatMap { getLoadObservable(loader!!, it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Ignore onNext event
|
||||
}, ReaderActivity::setInitialChapterError)
|
||||
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
||||
.flatMap { getLoadObservable(loader!!, it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Ignore onNext event
|
||||
}, ReaderActivity::setInitialChapterError)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -219,16 +232,16 @@ class ReaderPresenter(
|
||||
requiredLoadKey: String? = null
|
||||
): Observable<ViewerChapters> {
|
||||
return loader.loadChapter(chapter)
|
||||
.andThen(Observable.fromCallable {
|
||||
val chapterPos = chapterList.indexOf(chapter)
|
||||
.andThen(Observable.fromCallable {
|
||||
val chapterPos = chapterList.indexOf(chapter)
|
||||
|
||||
ViewerChapters(chapter,
|
||||
chapterList.getOrNull(chapterPos - 1),
|
||||
chapterList.getOrNull(chapterPos + 1))
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { newChapters ->
|
||||
// Add new references first to avoid unnecessary recycling
|
||||
ViewerChapters(chapter,
|
||||
chapterList.getOrNull(chapterPos - 1),
|
||||
chapterList.getOrNull(chapterPos + 1))
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { newChapters ->
|
||||
// Add new references first to avoid unnecessary recycling
|
||||
newChapters.ref()
|
||||
|
||||
// Ensure that we haven't made another load request in the meantime
|
||||
@ -259,10 +272,10 @@ class ReaderPresenter(
|
||||
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
activeChapterSubscription = getLoadObservable(loader, chapter, newLoadKey)
|
||||
.toCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
.toCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -277,13 +290,13 @@ class ReaderPresenter(
|
||||
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
||||
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.moveToPageIndex(0)
|
||||
}, { _, _ ->
|
||||
// Ignore onError event, viewers handle that state
|
||||
})
|
||||
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
||||
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.moveToPageIndex(0)
|
||||
}, { _, _ ->
|
||||
// Ignore onError event, viewers handle that state
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -300,12 +313,12 @@ class ReaderPresenter(
|
||||
val loader = loader ?: return
|
||||
|
||||
loader.loadChapter(chapter)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Update current chapters whenever a chapter is preloaded
|
||||
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Update current chapters whenever a chapter is preloaded
|
||||
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -346,9 +359,9 @@ class ReaderPresenter(
|
||||
*/
|
||||
private fun saveChapterProgress(chapter: ReaderChapter) {
|
||||
db.updateChapterProgress(chapter.chapter).asRxCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -357,9 +370,9 @@ class ReaderPresenter(
|
||||
private fun saveChapterHistory(chapter: ReaderChapter) {
|
||||
val history = History.create(chapter.chapter).apply { last_read = Date().time }
|
||||
db.updateHistoryLastRead(history).asRxCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -409,18 +422,18 @@ class ReaderPresenter(
|
||||
db.updateMangaViewer(manga).executeAsBlocking()
|
||||
|
||||
Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
val currChapters = viewerChaptersRelay.value
|
||||
if (currChapters != null) {
|
||||
// Save current page
|
||||
val currChapter = currChapters.currChapter
|
||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||
.subscribeFirst({ view, _ ->
|
||||
val currChapters = viewerChaptersRelay.value
|
||||
if (currChapters != null) {
|
||||
// Save current page
|
||||
val currChapter = currChapters.currChapter
|
||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||
|
||||
// Emit manga and chapters to the new viewer
|
||||
view.setManga(manga)
|
||||
view.setChapters(currChapters)
|
||||
}
|
||||
})
|
||||
// Emit manga and chapters to the new viewer
|
||||
view.setManga(manga)
|
||||
view.setChapters(currChapters)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -461,22 +474,22 @@ class ReaderPresenter(
|
||||
|
||||
// Pictures directory.
|
||||
val destDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + Environment.DIRECTORY_PICTURES +
|
||||
File.separator + "TachiyomiEH")
|
||||
File.separator + Environment.DIRECTORY_PICTURES +
|
||||
File.separator + "TachiyomiEH")
|
||||
|
||||
// Copy file in background.
|
||||
Observable.fromCallable { saveImage(page, destDir, manga) }
|
||||
.doOnNext { file ->
|
||||
DiskUtil.scanMedia(context, file)
|
||||
notifier.onComplete(file)
|
||||
}
|
||||
.doOnError { notifier.onError(it.message) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
||||
)
|
||||
.doOnNext { file ->
|
||||
DiskUtil.scanMedia(context, file)
|
||||
notifier.onComplete(file)
|
||||
}
|
||||
.doOnError { notifier.onError(it.message) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -493,14 +506,14 @@ class ReaderPresenter(
|
||||
|
||||
val destDir = File(context.cacheDir, "shared_image")
|
||||
|
||||
Observable.fromCallable { destDir.delete() } // Keep only the last shared file
|
||||
.map { saveImage(page, destDir, manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onShareImageResult(file) },
|
||||
{ view, error -> /* Empty */ }
|
||||
)
|
||||
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
||||
.map { saveImage(page, destDir, manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onShareImageResult(file) },
|
||||
{ view, error -> /* Empty */ }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -512,28 +525,28 @@ class ReaderPresenter(
|
||||
val stream = page.stream ?: return
|
||||
|
||||
Observable
|
||||
.fromCallable {
|
||||
if (manga.source == LocalSource.ID) {
|
||||
val context = Injekt.get<Application>()
|
||||
LocalSource.updateCover(context, manga, stream())
|
||||
R.string.cover_updated
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
||||
if (manga.favorite) {
|
||||
coverCache.copyToCache(thumbUrl, stream())
|
||||
.fromCallable {
|
||||
if (manga.source == LocalSource.ID) {
|
||||
val context = Injekt.get<Application>()
|
||||
LocalSource.updateCover(context, manga, stream())
|
||||
R.string.cover_updated
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
SetAsCoverResult.AddToLibraryFirst
|
||||
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
||||
if (manga.favorite) {
|
||||
coverCache.copyToCache(thumbUrl, stream())
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
SetAsCoverResult.AddToLibraryFirst
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, result -> view.onSetAsCoverResult(result) },
|
||||
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) }
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, result -> view.onSetAsCoverResult(result) },
|
||||
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -574,26 +587,26 @@ class ReaderPresenter(
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
|
||||
db.getTracks(manga).asRxSingle()
|
||||
.flatMapCompletable { trackList ->
|
||||
Completable.concat(trackList.map { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged && lastChapterRead > track.last_chapter_read) {
|
||||
track.last_chapter_read = lastChapterRead
|
||||
.flatMapCompletable { trackList ->
|
||||
Completable.concat(trackList.map { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged && lastChapterRead > track.last_chapter_read) {
|
||||
track.last_chapter_read = lastChapterRead
|
||||
|
||||
// We wan't these to execute even if the presenter is destroyed and leaks
|
||||
// for a while. The view can still be garbage collected.
|
||||
Observable.defer { service.update(track) }
|
||||
.map { db.insertTrack(track).executeAsBlocking() }
|
||||
.toCompletable()
|
||||
.onErrorComplete()
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
})
|
||||
}
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
// We wan't these to execute even if the presenter is destroyed and leaks
|
||||
// for a while. The view can still be garbage collected.
|
||||
Observable.defer { service.update(track) }
|
||||
.map { db.insertTrack(track).executeAsBlocking() }
|
||||
.toCompletable()
|
||||
.onErrorComplete()
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
})
|
||||
}
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -609,19 +622,19 @@ class ReaderPresenter(
|
||||
if (removeAfterReadSlots == -1) return
|
||||
|
||||
Completable
|
||||
.fromCallable {
|
||||
// Position of the read chapter
|
||||
val position = chapterList.indexOf(chapter)
|
||||
.fromCallable {
|
||||
// Position of the read chapter
|
||||
val position = chapterList.indexOf(chapter)
|
||||
|
||||
// Retrieve chapter to delete according to preference
|
||||
val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots)
|
||||
if (chapterToDelete != null) {
|
||||
downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga)
|
||||
// Retrieve chapter to delete according to preference
|
||||
val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots)
|
||||
if (chapterToDelete != null) {
|
||||
downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -630,9 +643,9 @@ class ReaderPresenter(
|
||||
*/
|
||||
private fun deletePendingChapters() {
|
||||
Completable.fromCallable { downloadManager.deletePendingChapters() }
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ class SettingsAdvancedController : SettingsController() {
|
||||
titleRes = R.string.pref_clear_cookies
|
||||
|
||||
onClick {
|
||||
network.cookies.removeAll()
|
||||
network.cookieManager.removeAll()
|
||||
activity?.toast(R.string.cookies_cleared)
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class SettingsGeneralController : SettingsController() {
|
||||
titleRes = R.string.pref_language
|
||||
entryValues = arrayOf("", "ar", "bg", "bn", "ca", "cs", "de", "el", "en-US", "en-GB",
|
||||
"es", "fr", "hi", "hu", "in", "it", "ja", "ko", "lv", "ms", "nb-rNO", "nl", "pl", "pt",
|
||||
"pt-BR", "ro", "ru", "sr", "sv", "th", "tr", "uk", "vi", "zh-rCN")
|
||||
"pt-BR", "ro", "ru", "sc", "sr", "sv", "th", "tl", "tr", "uk", "vi", "zh-rCN")
|
||||
entries = entryValues.map { value ->
|
||||
val locale = LocaleHelper.getLocaleFromString(value.toString())
|
||||
locale?.getDisplayName(locale)?.capitalize() ?:
|
||||
|
@ -64,6 +64,11 @@ class SettingsReaderController : SettingsController() {
|
||||
defaultValue = "500"
|
||||
summary = "%s"
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.skipRead
|
||||
titleRes = R.string.pref_skip_read_chapters
|
||||
defaultValue = false
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.fullscreen
|
||||
titleRes = R.string.pref_fullscreen
|
||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||
import eu.kanade.tachiyomi.data.track.shikomori.ShikomoriApi
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||
@ -53,6 +54,15 @@ class SettingsTrackingController : SettingsController(),
|
||||
dialog.showDialog(router)
|
||||
}
|
||||
}
|
||||
trackPreference(trackManager.shikomori) {
|
||||
onClick {
|
||||
val tabsIntent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
tabsIntent.launchUrl(activity, ShikomoriApi.authUrl())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +80,7 @@ class SettingsTrackingController : SettingsController(),
|
||||
super.onActivityResumed(activity)
|
||||
// Manually refresh anilist holder
|
||||
updatePreference(trackManager.aniList.id)
|
||||
updatePreference(trackManager.shikomori.id)
|
||||
}
|
||||
|
||||
private fun updatePreference(id: Int) {
|
||||
|
@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikomoriLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
val view = ProgressBar(this)
|
||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.shikomori.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
returnToSettings()
|
||||
}, {
|
||||
returnToSettings()
|
||||
})
|
||||
} else {
|
||||
trackManager.shikomori.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun returnToSettings() {
|
||||
finish()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.webkit.*
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
abstract class WebViewClientCompat : WebViewClient() {
|
||||
|
||||
open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
final override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): Boolean {
|
||||
return shouldOverrideUrlCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return shouldOverrideUrlCompat(view, url)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
final override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
return shouldInterceptRequestCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
final override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
url: String
|
||||
): WebResourceResponse? {
|
||||
return shouldInterceptRequestCompat(view, url)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
final override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError
|
||||
) {
|
||||
onReceivedErrorCompat(view, error.errorCode, error.description?.toString(),
|
||||
request.url.toString(), request.isForMainFrame)
|
||||
}
|
||||
|
||||
final override fun onReceivedError(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String
|
||||
) {
|
||||
onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
final override fun onReceivedHttpError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceResponse
|
||||
) {
|
||||
onReceivedErrorCompat(view, error.statusCode, error.reasonPhrase, request.url
|
||||
.toString(), request.isForMainFrame)
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user