mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 20:31:51 +01:00
Kissmanga loading through Cloudflare. A lot of refactoring was needed
This commit is contained in:
parent
8da11dbdb9
commit
6e8a41f898
@ -118,7 +118,6 @@ dependencies {
|
||||
|
||||
// Network client
|
||||
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
|
||||
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
|
||||
|
||||
// REST
|
||||
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
||||
@ -131,6 +130,9 @@ dependencies {
|
||||
// JSON
|
||||
compile 'com.google.code.gson:gson:2.6.2'
|
||||
|
||||
// JavaScript engine
|
||||
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
||||
|
||||
// Disk cache
|
||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||
|
||||
@ -154,6 +156,7 @@ dependencies {
|
||||
|
||||
// Image library
|
||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
||||
|
||||
// Logging
|
||||
compile 'com.jakewharton.timber:timber:4.1.2'
|
||||
|
@ -101,7 +101,7 @@
|
||||
|
||||
|
||||
<meta-data
|
||||
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
|
||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||
android:value="GlideModule" />
|
||||
|
||||
</application>
|
||||
|
@ -1,11 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import eu.kanade.tachiyomi.util.DiskUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@ -27,80 +22,19 @@ class CoverCache(private val context: Context) {
|
||||
*/
|
||||
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
|
||||
|
||||
/**
|
||||
* Download the cover with Glide and save the file.
|
||||
* @param thumbnailUrl url of thumbnail.
|
||||
* @param headers headers included in Glide request.
|
||||
* @param onReady function to call when the image is ready
|
||||
*/
|
||||
fun save(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)? = null) {
|
||||
// Check if url is empty.
|
||||
if (thumbnailUrl.isNullOrEmpty())
|
||||
return
|
||||
|
||||
// Download the cover with Glide and save the file.
|
||||
val url = GlideUrl(thumbnailUrl, headers)
|
||||
Glide.with(context)
|
||||
.load(url)
|
||||
.downloadOnly(object : SimpleTarget<File>() {
|
||||
override fun onResourceReady(resource: File, anim: GlideAnimation<in File>) {
|
||||
try {
|
||||
// Copy the cover from Glide's cache to local cache.
|
||||
copyToCache(thumbnailUrl!!, resource)
|
||||
|
||||
onReady?.invoke(resource)
|
||||
} catch (e: IOException) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or load the image from cache
|
||||
* @param thumbnailUrl the thumbnail url.
|
||||
* @param headers headers included in Glide request.
|
||||
* @param onReady function to call when the image is ready
|
||||
*/
|
||||
fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)?) {
|
||||
// Check if url is empty.
|
||||
if (thumbnailUrl.isNullOrEmpty())
|
||||
return
|
||||
|
||||
// If file exist load it otherwise save it.
|
||||
val localCover = getCoverFromCache(thumbnailUrl!!)
|
||||
if (localCover.exists()) {
|
||||
onReady?.invoke(localCover)
|
||||
} else {
|
||||
save(thumbnailUrl, headers, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cover from cache.
|
||||
*
|
||||
* @param thumbnailUrl the thumbnail url.
|
||||
* @return cover image.
|
||||
*/
|
||||
private fun getCoverFromCache(thumbnailUrl: String): File {
|
||||
fun getCoverFile(thumbnailUrl: String): File {
|
||||
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the given file to this cache.
|
||||
* @param thumbnailUrl url of thumbnail.
|
||||
* @param sourceFile the source file of the cover image.
|
||||
* @throws IOException if there's any error.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun copyToCache(thumbnailUrl: String, sourceFile: File) {
|
||||
// Get destination file.
|
||||
val destFile = getCoverFromCache(thumbnailUrl)
|
||||
|
||||
sourceFile.copyTo(destFile, overwrite = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the given stream to this cache.
|
||||
*
|
||||
* @param thumbnailUrl url of the thumbnail.
|
||||
* @param inputStream the stream to copy.
|
||||
* @throws IOException if there's any error.
|
||||
@ -108,13 +42,14 @@ class CoverCache(private val context: Context) {
|
||||
@Throws(IOException::class)
|
||||
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
||||
// Get destination file.
|
||||
val destFile = getCoverFromCache(thumbnailUrl)
|
||||
val destFile = getCoverFile(thumbnailUrl)
|
||||
|
||||
destFile.outputStream().use { inputStream.copyTo(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cover file from the cache.
|
||||
*
|
||||
* @param thumbnailUrl the thumbnail url.
|
||||
* @return status of deletion.
|
||||
*/
|
||||
@ -124,7 +59,7 @@ class CoverCache(private val context: Context) {
|
||||
return false
|
||||
|
||||
// Remove file.
|
||||
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
||||
val file = getCoverFile(thumbnailUrl!!)
|
||||
return file.exists() && file.delete()
|
||||
}
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.module.GlideModule
|
||||
|
||||
/**
|
||||
* Class used to update Glide module settings
|
||||
*/
|
||||
class CoverGlideModule : GlideModule {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
// Set the cache size of Glide to 15 MiB
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide) {
|
||||
// Nothing to see here!
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.GlideModule
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Class used to update Glide module settings
|
||||
*/
|
||||
class AppGlideModule : GlideModule {
|
||||
|
||||
@Inject lateinit var networkHelper: NetworkHelper
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
// Set the cache size of Glide to 15 MiB
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide) {
|
||||
App.get(context).component.inject(this)
|
||||
glide.register(GlideUrl::class.java, InputStream::class.java,
|
||||
OkHttpUrlLoader.Factory(networkHelper.defaultClient))
|
||||
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
|
||||
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
|
||||
* fallbacks to network and copies it to the cache.
|
||||
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
|
||||
* to network for fetching.
|
||||
*
|
||||
* @param networkFetcher the network fetcher for this cover.
|
||||
* @param file the file where this cover should be. It may exists or not.
|
||||
* @param manga the manga of the cover to load.
|
||||
*/
|
||||
class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val file: File,
|
||||
private val manga: Manga)
|
||||
: DataFetcher<InputStream> {
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun loadData(priority: Priority): InputStream? {
|
||||
if (manga.favorite) {
|
||||
if (!file.exists()) {
|
||||
file.parentFile.mkdirs()
|
||||
networkFetcher.loadData(priority)?.let {
|
||||
it.use { input ->
|
||||
file.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return FileInputStream(file)
|
||||
} else {
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
return networkFetcher.loadData(priority)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id for this manga's cover.
|
||||
*
|
||||
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
|
||||
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
|
||||
* the file has changed. If the file doesn't exist it will append a 0.
|
||||
*/
|
||||
override fun getId(): String {
|
||||
return manga.thumbnail_url + file.lastModified()
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
networkFetcher.cancel()
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
networkFetcher.cleanup()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import com.bumptech.glide.load.model.*
|
||||
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||
* Coupled with [MangaDataFetcher], this class allows to implement the following flow:
|
||||
*
|
||||
* - Check in RAM LRU.
|
||||
* - Check in disk LRU.
|
||||
* - Check in this module.
|
||||
* - Fetch from the network connection.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
|
||||
/**
|
||||
* Cover cache where persistent covers are stored.
|
||||
*/
|
||||
@Inject lateinit var coverCache: CoverCache
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
@Inject lateinit var sourceManager: SourceManager
|
||||
|
||||
/**
|
||||
* Base network loader.
|
||||
*/
|
||||
private val baseLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
||||
InputStream::class.java, context)
|
||||
|
||||
/**
|
||||
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
||||
* and the file where it should be stored in case the manga is a favorite.
|
||||
*/
|
||||
private val modelCache = ModelCache<String, Pair<GlideUrl, File>>(100)
|
||||
|
||||
/**
|
||||
* Map where request headers are stored for a source.
|
||||
*/
|
||||
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
|
||||
|
||||
init {
|
||||
App.get(context).component.inject(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory class for creating [MangaModelLoader] instances.
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
||||
|
||||
override fun build(context: Context, factories: GenericLoaderFactory)
|
||||
= MangaModelLoader(context)
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [MangaDataFetcher] for the given manga or null if the url is empty.
|
||||
*
|
||||
* @param manga the model.
|
||||
* @param width the width of the view where the resource will be loaded.
|
||||
* @param height the height of the view where the resource will be loaded.
|
||||
*/
|
||||
override fun getResourceFetcher(manga: Manga,
|
||||
width: Int,
|
||||
height: Int): DataFetcher<InputStream>? {
|
||||
// Check thumbnail is not null or empty
|
||||
val url = manga.thumbnail_url
|
||||
if (url.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Obtain the request url and the file for this url from the LRU cache, or calculate it
|
||||
// and add them to the cache.
|
||||
val (glideUrl, file) = modelCache.get(url, width, height) ?:
|
||||
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url)).apply {
|
||||
modelCache.put(url, width, height, this)
|
||||
}
|
||||
|
||||
// Get the network fetcher for this request url.
|
||||
val networkFetcher = baseLoader.getResourceFetcher(glideUrl, width, height)
|
||||
|
||||
// Return an instance of our fetcher providing the needed elements.
|
||||
return MangaDataFetcher(networkFetcher, file, manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request headers for a source copying its OkHttp headers and caching them.
|
||||
*
|
||||
* @param manga the model.
|
||||
*/
|
||||
fun getHeaders(manga: Manga): LazyHeaders {
|
||||
return cachedHeaders.getOrPut(manga.source) {
|
||||
val source = sourceManager.get(manga.source)!!
|
||||
|
||||
LazyHeaders.Builder().apply {
|
||||
for ((key, value) in source.requestHeaders.toMultimap()) {
|
||||
addHeader(key, value[0])
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -102,7 +102,7 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
|
||||
|
||||
// MAL doesn't support score with decimals
|
||||
fun getList(): Observable<List<MangaSync>> {
|
||||
return networkService.requestBody(get(getListUrl(username), headers), true)
|
||||
return networkService.requestBody(get(getListUrl(username), headers), networkService.forceCacheClient)
|
||||
.map { Jsoup.parse(it) }
|
||||
.flatMap { Observable.from(it.select("manga")) }
|
||||
.map {
|
||||
|
@ -0,0 +1,81 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import android.net.Uri
|
||||
import com.squareup.duktape.Duktape
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
object CloudflareScraper {
|
||||
|
||||
//language=RegExp
|
||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var t,r,a,f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||
|
||||
//language=RegExp
|
||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||
|
||||
//language=RegExp
|
||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||
|
||||
fun request(chain: Interceptor.Chain, cookies: PersistentCookieStore): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
// Check if we already solved a challenge
|
||||
if (response.code() != 502 &&
|
||||
cookies.get(response.request().url()).find { it.name() == "cf_clearance" } != null) {
|
||||
return response
|
||||
}
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
if ("URL=/cdn-cgi/" in response.header("Refresh", "")
|
||||
&& response.header("Server", "") == "cloudflare-nginx") {
|
||||
return chain.proceed(resolveChallenge(response))
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private fun resolveChallenge(response: Response): Request {
|
||||
val duktape = Duktape.create()
|
||||
try {
|
||||
val originalRequest = response.request()
|
||||
val domain = originalRequest.url().host()
|
||||
val content = response.body().string()
|
||||
|
||||
// CloudFlare requires waiting 5 seconds before resolving the challenge
|
||||
Thread.sleep(5000)
|
||||
|
||||
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
|
||||
|
||||
if (operation == null || challenge == null || pass == null) {
|
||||
throw RuntimeException("Failed resolving Cloudflare challenge")
|
||||
}
|
||||
|
||||
val js = operation
|
||||
//language=RegExp
|
||||
.replace(Regex("""a\.value =(.+?) \+ .+?;"""), "$1")
|
||||
//language=RegExp
|
||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||
.replace("\n", "")
|
||||
|
||||
// Duktape can only return strings, so the result has to be converted to string first
|
||||
val result = duktape.evaluate("$js.toString()").toInt()
|
||||
|
||||
val answer = "${result + domain.length}"
|
||||
|
||||
val url = Uri.parse("http://$domain/cdn-cgi/l/chk_jschl").buildUpon()
|
||||
.appendQueryParameter("jschl_vc", challenge)
|
||||
.appendQueryParameter("pass", pass)
|
||||
.appendQueryParameter("jschl_answer", answer)
|
||||
.toString()
|
||||
|
||||
val referer = originalRequest.url().toString()
|
||||
return get(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
|
||||
} finally {
|
||||
duktape.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.*
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import java.io.File
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.net.CookieStore
|
||||
|
||||
class NetworkHelper(context: Context) {
|
||||
|
||||
@ -14,43 +14,41 @@ class NetworkHelper(context: Context) {
|
||||
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
private val cookieManager = CookieManager().apply {
|
||||
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
}
|
||||
private val cookieManager = PersistentCookieJar(context)
|
||||
|
||||
private val forceCacheInterceptor = { chain: Interceptor.Chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.removeHeader("Pragma")
|
||||
.header("Cache-Control", "max-age=" + 600)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||
val defaultClient = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.cache(Cache(cacheDir, cacheSize))
|
||||
.build()
|
||||
|
||||
private val forceCacheClient = client.newBuilder()
|
||||
.addNetworkInterceptor(forceCacheInterceptor)
|
||||
val forceCacheClient = defaultClient.newBuilder()
|
||||
.addNetworkInterceptor({ chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.removeHeader("Pragma")
|
||||
.header("Cache-Control", "max-age=" + 600)
|
||||
.build()
|
||||
})
|
||||
.build()
|
||||
|
||||
val cookies: CookieStore
|
||||
get() = cookieManager.cookieStore
|
||||
val cloudflareClient = defaultClient.newBuilder()
|
||||
.addInterceptor { CloudflareScraper.request(it, cookies) }
|
||||
.build()
|
||||
|
||||
val cookies: PersistentCookieStore
|
||||
get() = cookieManager.store
|
||||
|
||||
@JvmOverloads
|
||||
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
|
||||
fun request(request: Request, client: OkHttpClient = defaultClient): Observable<Response> {
|
||||
return Observable.fromCallable {
|
||||
val c = if (forceCache) forceCacheClient else client
|
||||
c.newCall(request).execute().apply { body().close() }
|
||||
client.newCall(request).execute().apply { body().close() }
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
|
||||
fun requestBody(request: Request, client: OkHttpClient = defaultClient): Observable<String> {
|
||||
return Observable.fromCallable {
|
||||
val c = if (forceCache) forceCacheClient else client
|
||||
c.newCall(request).execute().body().string()
|
||||
client.newCall(request).execute().body().string()
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +57,7 @@ class NetworkHelper(context: Context) {
|
||||
}
|
||||
|
||||
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
|
||||
val progressClient = client.newBuilder()
|
||||
val progressClient = defaultClient.newBuilder()
|
||||
.cache(null)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
@ -72,5 +70,4 @@ class NetworkHelper(context: Context) {
|
||||
return progressClient.newCall(request).execute()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
package eu.kanade.tachiyomi.data.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package eu.kanade.tachiyomi.data.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")
|
||||
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap.put(key, nonExpiredCookies)
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
||||
synchronized(this) {
|
||||
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 { it.toString() }
|
||||
.toSet()
|
||||
|
||||
prefs.edit().putStringSet(key, newValues).apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
synchronized(this) {
|
||||
prefs.edit().clear().apply()
|
||||
cookieMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
|
||||
fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
||||
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.source.base
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
@ -11,6 +10,7 @@ import eu.kanade.tachiyomi.data.network.get
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
@ -27,12 +27,13 @@ abstract class Source(context: Context) : BaseSource() {
|
||||
|
||||
val requestHeaders by lazy { headersBuilder().build() }
|
||||
|
||||
val glideHeaders by lazy { glideHeadersBuilder().build() }
|
||||
|
||||
init {
|
||||
App.get(context).component.inject(this)
|
||||
}
|
||||
|
||||
open val networkClient: OkHttpClient
|
||||
get() = networkService.defaultClient
|
||||
|
||||
override fun isLoginRequired(): Boolean {
|
||||
return false
|
||||
}
|
||||
@ -75,7 +76,7 @@ abstract class Source(context: Context) : BaseSource() {
|
||||
|
||||
// Get the most popular mangas from the source
|
||||
open fun pullPopularMangasFromNetwork(page: MangasPage): Observable<MangasPage> {
|
||||
return networkService.requestBody(popularMangaRequest(page), true)
|
||||
return networkService.requestBody(popularMangaRequest(page), networkClient)
|
||||
.map { Jsoup.parse(it) }
|
||||
.doOnNext { doc -> page.mangas = parsePopularMangasFromHtml(doc) }
|
||||
.doOnNext { doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page) }
|
||||
@ -84,7 +85,7 @@ abstract class Source(context: Context) : BaseSource() {
|
||||
|
||||
// Get mangas from the source with a query
|
||||
open fun searchMangasFromNetwork(page: MangasPage, query: String): Observable<MangasPage> {
|
||||
return networkService.requestBody(searchMangaRequest(page, query), true)
|
||||
return networkService.requestBody(searchMangaRequest(page, query), networkClient)
|
||||
.map { Jsoup.parse(it) }
|
||||
.doOnNext { doc -> page.mangas = parseSearchFromHtml(doc) }
|
||||
.doOnNext { doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query) }
|
||||
@ -93,13 +94,13 @@ abstract class Source(context: Context) : BaseSource() {
|
||||
|
||||
// Get manga details from the source
|
||||
open fun pullMangaFromNetwork(mangaUrl: String): Observable<Manga> {
|
||||
return networkService.requestBody(mangaDetailsRequest(mangaUrl))
|
||||
return networkService.requestBody(mangaDetailsRequest(mangaUrl), networkClient)
|
||||
.flatMap { Observable.just(parseHtmlToManga(mangaUrl, it)) }
|
||||
}
|
||||
|
||||
// Get chapter list of a manga from the source
|
||||
open fun pullChaptersFromNetwork(mangaUrl: String): Observable<List<Chapter>> {
|
||||
return networkService.requestBody(chapterListRequest(mangaUrl))
|
||||
return networkService.requestBody(chapterListRequest(mangaUrl), networkClient)
|
||||
.flatMap { unparsedHtml ->
|
||||
val chapters = parseHtmlToChapters(unparsedHtml)
|
||||
if (!chapters.isEmpty())
|
||||
@ -116,7 +117,7 @@ abstract class Source(context: Context) : BaseSource() {
|
||||
}
|
||||
|
||||
open fun pullPageListFromNetwork(chapterUrl: String): Observable<List<Page>> {
|
||||
return networkService.requestBody(pageListRequest(chapterUrl))
|
||||
return networkService.requestBody(pageListRequest(chapterUrl), networkClient)
|
||||
.flatMap { unparsedHtml ->
|
||||
val pages = convertToPages(parseHtmlToPageUrls(unparsedHtml))
|
||||
if (!pages.isEmpty())
|
||||
@ -141,7 +142,7 @@ abstract class Source(context: Context) : BaseSource() {
|
||||
|
||||
open fun getImageUrlFromPage(page: Page): Observable<Page> {
|
||||
page.status = Page.LOAD_PAGE
|
||||
return networkService.requestBody(imageUrlRequest(page))
|
||||
return networkService.requestBody(imageUrlRequest(page), networkClient)
|
||||
.flatMap { unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)) }
|
||||
.onErrorResumeNext { e ->
|
||||
page.status = Page.ERROR
|
||||
@ -224,13 +225,4 @@ abstract class Source(context: Context) : BaseSource() {
|
||||
|
||||
}
|
||||
|
||||
protected open fun glideHeadersBuilder(): LazyHeaders.Builder {
|
||||
val builder = LazyHeaders.Builder()
|
||||
for ((key, value) in requestHeaders.toMultimap()) {
|
||||
builder.addHeader(key, value[0])
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import java.net.HttpCookie;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.ParseException;
|
||||
@ -34,6 +33,7 @@ import eu.kanade.tachiyomi.data.source.base.LoginSource;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.util.Parser;
|
||||
import okhttp3.Cookie;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Request;
|
||||
@ -358,8 +358,8 @@ public class Batoto extends LoginSource {
|
||||
@Override
|
||||
public boolean isLogged() {
|
||||
try {
|
||||
for ( HttpCookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL)) ) {
|
||||
if (cookie.getName().equals("pass_hash"))
|
||||
for (Cookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL))) {
|
||||
if (cookie.name().equals("pass_hash"))
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,234 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.network.ReqKt;
|
||||
import eu.kanade.tachiyomi.data.source.Language;
|
||||
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.util.Parser;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Request;
|
||||
|
||||
public class Kissmanga extends Source {
|
||||
|
||||
public static final String NAME = "Kissmanga";
|
||||
public static final String HOST = "kissmanga.com";
|
||||
public static final String IP = "93.174.95.110";
|
||||
public static final String BASE_URL = "http://" + IP;
|
||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/MangaList/MostPopular?page=%s";
|
||||
public static final String SEARCH_URL = BASE_URL + "/AdvanceSearch";
|
||||
|
||||
public Kissmanga(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Headers.Builder headersBuilder() {
|
||||
Headers.Builder builder = super.headersBuilder();
|
||||
builder.add("Host", HOST);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
public Language getLang() {
|
||||
return LanguageKt.getEN();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialPopularMangasUrl() {
|
||||
return String.format(POPULAR_MANGAS_URL, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialSearchUrl(String query) {
|
||||
return SEARCH_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request searchMangaRequest(MangasPage page, String query) {
|
||||
if (page.page == 1) {
|
||||
page.url = getInitialSearchUrl(query);
|
||||
}
|
||||
|
||||
FormBody.Builder form = new FormBody.Builder();
|
||||
form.add("authorArtist", "");
|
||||
form.add("mangaName", query);
|
||||
form.add("status", "");
|
||||
form.add("genres", "");
|
||||
|
||||
return ReqKt.post(page.url, getRequestHeaders(), form.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request pageListRequest(String chapterUrl) {
|
||||
return ReqKt.post(getBaseUrl() + chapterUrl, getRequestHeaders());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request imageRequest(Page page) {
|
||||
return ReqKt.get(page.getImageUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||
List<Manga> mangaList = new ArrayList<>();
|
||||
|
||||
for (Element currentHtmlBlock : parsedHtml.select("table.listing tr:gt(1)")) {
|
||||
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
||||
mangaList.add(manga);
|
||||
}
|
||||
|
||||
return mangaList;
|
||||
}
|
||||
|
||||
private Manga constructPopularMangaFromHtml(Element htmlBlock) {
|
||||
Manga manga = new Manga();
|
||||
manga.source = getId();
|
||||
|
||||
Element urlElement = Parser.element(htmlBlock, "td a:eq(0)");
|
||||
|
||||
if (urlElement != null) {
|
||||
manga.setUrl(urlElement.attr("href"));
|
||||
manga.title = urlElement.text();
|
||||
}
|
||||
|
||||
return manga;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||
String path = Parser.href(parsedHtml, "li > a:contains(› Next)");
|
||||
return path != null ? BASE_URL + path : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||
return parsePopularMangasFromHtml(parsedHtml);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
Element infoElement = parsedDocument.select("div.barContent").first();
|
||||
|
||||
Manga manga = Manga.create(mangaUrl);
|
||||
manga.title = Parser.text(infoElement, "a.bigChar");
|
||||
manga.author = Parser.text(infoElement, "p:has(span:contains(Author:)) > a");
|
||||
manga.genre = Parser.allText(infoElement, "p:has(span:contains(Genres:)) > *:gt(0)");
|
||||
manga.description = Parser.allText(infoElement, "p:has(span:contains(Summary:)) ~ p");
|
||||
manga.status = parseStatus(Parser.text(infoElement, "p:has(span:contains(Status:))"));
|
||||
|
||||
String thumbnail = Parser.src(parsedDocument, ".rightBox:eq(0) img");
|
||||
if (thumbnail != null) {
|
||||
manga.thumbnail_url = Uri.parse(thumbnail).buildUpon().authority(IP).toString();
|
||||
}
|
||||
|
||||
manga.initialized = true;
|
||||
return manga;
|
||||
}
|
||||
|
||||
private int parseStatus(String status) {
|
||||
if (status.contains("Ongoing")) {
|
||||
return Manga.ONGOING;
|
||||
}
|
||||
if (status.contains("Completed")) {
|
||||
return Manga.COMPLETED;
|
||||
}
|
||||
return Manga.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
List<Chapter> chapterList = new ArrayList<>();
|
||||
|
||||
for (Element chapterElement : parsedDocument.select("table.listing tr:gt(1)")) {
|
||||
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
||||
chapterList.add(chapter);
|
||||
}
|
||||
|
||||
return chapterList;
|
||||
}
|
||||
|
||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||
Chapter chapter = Chapter.create();
|
||||
|
||||
Element urlElement = Parser.element(chapterElement, "a");
|
||||
String date = Parser.text(chapterElement, "td:eq(1)");
|
||||
|
||||
if (urlElement != null) {
|
||||
chapter.setUrl(urlElement.attr("href"));
|
||||
chapter.name = urlElement.text();
|
||||
}
|
||||
if (date != null) {
|
||||
try {
|
||||
chapter.date_upload = new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH).parse(date).getTime();
|
||||
} catch (ParseException e) { /* Ignore */ }
|
||||
}
|
||||
return chapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
List<String> pageUrlList = new ArrayList<>();
|
||||
|
||||
int numImages = parsedDocument.select("#divImage img").size();
|
||||
|
||||
for (int i = 0; i < numImages; i++) {
|
||||
pageUrlList.add("");
|
||||
}
|
||||
return pageUrlList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
||||
Pattern p = Pattern.compile("lstImages.push\\(\"(.+?)\"");
|
||||
Matcher m = p.matcher(unparsedHtml);
|
||||
|
||||
int i = 0;
|
||||
while (m.find()) {
|
||||
pages.get(i++).setImageUrl(m.group(1));
|
||||
}
|
||||
return (List<Page>) pages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.get
|
||||
import eu.kanade.tachiyomi.data.network.post
|
||||
import eu.kanade.tachiyomi.data.source.EN
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.Parser
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Kissmanga(context: Context) : Source(context) {
|
||||
|
||||
override fun getName() = NAME
|
||||
|
||||
override fun getBaseUrl() = BASE_URL
|
||||
|
||||
override fun getLang() = EN
|
||||
|
||||
override val networkClient: OkHttpClient
|
||||
get() = networkService.cloudflareClient
|
||||
|
||||
override fun getInitialPopularMangasUrl(): String {
|
||||
return String.format(POPULAR_MANGAS_URL, 1)
|
||||
}
|
||||
|
||||
override fun getInitialSearchUrl(query: String): String {
|
||||
return SEARCH_URL
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = getInitialSearchUrl(query)
|
||||
}
|
||||
|
||||
val form = FormBody.Builder()
|
||||
form.add("authorArtist", "")
|
||||
form.add("mangaName", query)
|
||||
form.add("status", "")
|
||||
form.add("genres", "")
|
||||
|
||||
return post(page.url, requestHeaders, form.build())
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapterUrl: String): Request {
|
||||
return post(baseUrl + chapterUrl, requestHeaders)
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return get(page.imageUrl)
|
||||
}
|
||||
|
||||
override fun parsePopularMangasFromHtml(parsedHtml: Document): List<Manga> {
|
||||
val mangaList = ArrayList<Manga>()
|
||||
|
||||
for (currentHtmlBlock in parsedHtml.select("table.listing tr:gt(1)")) {
|
||||
val manga = constructPopularMangaFromHtml(currentHtmlBlock)
|
||||
mangaList.add(manga)
|
||||
}
|
||||
|
||||
return mangaList
|
||||
}
|
||||
|
||||
private fun constructPopularMangaFromHtml(htmlBlock: Element): Manga {
|
||||
val manga = Manga()
|
||||
manga.source = id
|
||||
|
||||
val urlElement = Parser.element(htmlBlock, "td a:eq(0)")
|
||||
|
||||
if (urlElement != null) {
|
||||
manga.setUrl(urlElement.attr("href"))
|
||||
manga.title = urlElement.text()
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun parseNextPopularMangasUrl(parsedHtml: Document, page: MangasPage): String? {
|
||||
val path = Parser.href(parsedHtml, "li > a:contains(› Next)")
|
||||
return if (path != null) BASE_URL + path else null
|
||||
}
|
||||
|
||||
override fun parseSearchFromHtml(parsedHtml: Document): List<Manga> {
|
||||
return parsePopularMangasFromHtml(parsedHtml)
|
||||
}
|
||||
|
||||
override fun parseNextSearchUrl(parsedHtml: Document, page: MangasPage, query: String): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun parseHtmlToManga(mangaUrl: String, unparsedHtml: String): Manga {
|
||||
val parsedDocument = Jsoup.parse(unparsedHtml)
|
||||
val infoElement = parsedDocument.select("div.barContent").first()
|
||||
|
||||
val manga = Manga.create(mangaUrl)
|
||||
manga.title = Parser.text(infoElement, "a.bigChar")
|
||||
manga.author = Parser.text(infoElement, "p:has(span:contains(Author:)) > a")
|
||||
manga.genre = Parser.allText(infoElement, "p:has(span:contains(Genres:)) > *:gt(0)")
|
||||
manga.description = Parser.allText(infoElement, "p:has(span:contains(Summary:)) ~ p")
|
||||
manga.status = parseStatus(Parser.text(infoElement, "p:has(span:contains(Status:))")!!)
|
||||
|
||||
val thumbnail = Parser.src(parsedDocument, ".rightBox:eq(0) img")
|
||||
if (thumbnail != null) {
|
||||
manga.thumbnail_url = thumbnail
|
||||
}
|
||||
|
||||
manga.initialized = true
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String): Int {
|
||||
if (status.contains("Ongoing")) {
|
||||
return Manga.ONGOING
|
||||
}
|
||||
if (status.contains("Completed")) {
|
||||
return Manga.COMPLETED
|
||||
}
|
||||
return Manga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun parseHtmlToChapters(unparsedHtml: String): List<Chapter> {
|
||||
val parsedDocument = Jsoup.parse(unparsedHtml)
|
||||
val chapterList = ArrayList<Chapter>()
|
||||
|
||||
for (chapterElement in parsedDocument.select("table.listing tr:gt(1)")) {
|
||||
val chapter = constructChapterFromHtmlBlock(chapterElement)
|
||||
chapterList.add(chapter)
|
||||
}
|
||||
|
||||
return chapterList
|
||||
}
|
||||
|
||||
private fun constructChapterFromHtmlBlock(chapterElement: Element): Chapter {
|
||||
val chapter = Chapter.create()
|
||||
|
||||
val urlElement = Parser.element(chapterElement, "a")
|
||||
val date = Parser.text(chapterElement, "td:eq(1)")
|
||||
|
||||
if (urlElement != null) {
|
||||
chapter.setUrl(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
}
|
||||
if (date != null) {
|
||||
try {
|
||||
chapter.date_upload = SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) { /* Ignore */
|
||||
}
|
||||
|
||||
}
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun parseHtmlToPageUrls(unparsedHtml: String): List<String> {
|
||||
val parsedDocument = Jsoup.parse(unparsedHtml)
|
||||
val pageUrlList = ArrayList<String>()
|
||||
|
||||
val numImages = parsedDocument.select("#divImage img").size
|
||||
|
||||
for (i in 0..numImages - 1) {
|
||||
pageUrlList.add("")
|
||||
}
|
||||
return pageUrlList
|
||||
}
|
||||
|
||||
override fun parseFirstPage(pages: List<Page>, unparsedHtml: String): List<Page> {
|
||||
val p = Pattern.compile("lstImages.push\\(\"(.+?)\"")
|
||||
val m = p.matcher(unparsedHtml)
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
pages[i++].imageUrl = m.group(1)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun parseHtmlToImageUrl(unparsedHtml: String): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val NAME = "Kissmanga"
|
||||
val BASE_URL = "http://kissmanga.com"
|
||||
val POPULAR_MANGAS_URL = BASE_URL + "/MangaList/MostPopular?page=%s"
|
||||
val SEARCH_URL = BASE_URL + "/AdvanceSearch"
|
||||
}
|
||||
|
||||
}
|
@ -100,7 +100,7 @@ public class ReadMangaToday extends Source {
|
||||
@Override
|
||||
public Observable<MangasPage> searchMangasFromNetwork(final MangasPage page, String query) {
|
||||
return networkService
|
||||
.requestBody(searchMangaRequest(page, query), true)
|
||||
.requestBody(searchMangaRequest(page, query), networkService.getDefaultClient())
|
||||
.doOnNext(new Action1<String>() {
|
||||
@Override
|
||||
public void call(String doc) {
|
||||
|
@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.injection.component
|
||||
|
||||
import android.app.Application
|
||||
import dagger.Component
|
||||
import eu.kanade.tachiyomi.data.glide.AppGlideModule
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.glide.MangaModelLoader
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||
@ -51,6 +53,9 @@ interface AppComponent {
|
||||
fun inject(downloadService: DownloadService)
|
||||
fun inject(updateMangaSyncService: UpdateMangaSyncService)
|
||||
|
||||
fun inject(mangaModelLoader: MangaModelLoader)
|
||||
fun inject(appGlideModule: AppGlideModule)
|
||||
|
||||
fun inject(updateDownloader: UpdateDownloader)
|
||||
fun application(): Application
|
||||
|
||||
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.catalogue
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
||||
|
||||
@ -42,20 +41,16 @@ class CatalogueGridHolder(private val view: View, private val adapter: Catalogue
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
fun setImage(manga: Manga) {
|
||||
Glide.clear(view.thumbnail)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
val url = manga.thumbnail_url!!
|
||||
val headers = adapter.fragment.presenter.source.glideHeaders
|
||||
|
||||
Glide.with(view.context)
|
||||
.load(if (headers != null) GlideUrl(url, headers) else url)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.centerCrop()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(view.thumbnail)
|
||||
|
||||
} else {
|
||||
Glide.clear(view.thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@ -38,6 +39,11 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
*/
|
||||
@Inject lateinit var prefs: PreferencesHelper
|
||||
|
||||
/**
|
||||
* Cover cache.
|
||||
*/
|
||||
@Inject lateinit var coverCache: CoverCache
|
||||
|
||||
/**
|
||||
* Enabled sources.
|
||||
*/
|
||||
@ -335,6 +341,9 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
*/
|
||||
fun changeMangaFavorite(manga: Manga) {
|
||||
manga.favorite = !manga.favorite
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
|
@ -98,10 +98,9 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
|
||||
* @param position the position to bind.
|
||||
*/
|
||||
override fun onBindViewHolder(holder: LibraryHolder, position: Int) {
|
||||
val presenter = (fragment.parentFragment as LibraryFragment).presenter
|
||||
val manga = getItem(position)
|
||||
|
||||
holder.onSetValues(manga, presenter)
|
||||
holder.onSetValues(manga)
|
||||
//When user scrolls this bind the correct selection status
|
||||
holder.itemView.isActivated = isSelected(position)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.event.LibraryMangaEvent
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryMangaEvent
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||
|
@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.event.LibraryMangaEvent
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryActivity
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
@ -388,7 +387,10 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
* @param mangas the manga list to move.
|
||||
*/
|
||||
private fun moveMangasToCategories(mangas: List<Manga>) {
|
||||
val categories = presenter.categories
|
||||
// Hide the default category because it has a different behavior than the ones from db.
|
||||
val categories = presenter.categories.filter { it.id != 0 }
|
||||
|
||||
// Get indexes of the common categories to preselect.
|
||||
val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
|
||||
.map { categories.indexOf(it) }
|
||||
.toTypedArray()
|
||||
@ -397,7 +399,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
.title(R.string.action_move_category)
|
||||
.items(categories.map { it.name })
|
||||
.itemsCallbackMultiChoice(commonCategoriesIndexes) { dialog, positions, text ->
|
||||
presenter.moveMangasToCategories(positions, mangas)
|
||||
val selectedCategories = positions.map { categories[it] }
|
||||
presenter.moveMangasToCategories(selectedCategories, mangas)
|
||||
destroyActionModeIfNeeded()
|
||||
true
|
||||
}
|
||||
|
@ -3,10 +3,7 @@ package eu.kanade.tachiyomi.ui.library
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.signature.StringSignature
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
||||
|
||||
@ -19,8 +16,10 @@ import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
class LibraryHolder(private val view: View, private val adapter: LibraryCategoryAdapter, listener: FlexibleViewHolder.OnListItemClickListener) :
|
||||
FlexibleViewHolder(view, adapter, listener) {
|
||||
class LibraryHolder(private val view: View,
|
||||
private val adapter: LibraryCategoryAdapter,
|
||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
||||
: FlexibleViewHolder(view, adapter, listener) {
|
||||
|
||||
private var manga: Manga? = null
|
||||
|
||||
@ -29,9 +28,8 @@ class LibraryHolder(private val view: View, private val adapter: LibraryCategory
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
* @param presenter the library presenter.
|
||||
*/
|
||||
fun onSetValues(manga: Manga, presenter: LibraryPresenter) {
|
||||
fun onSetValues(manga: Manga) {
|
||||
this.manga = manga
|
||||
|
||||
// Update the title of the manga.
|
||||
@ -44,31 +42,13 @@ class LibraryHolder(private val view: View, private val adapter: LibraryCategory
|
||||
}
|
||||
|
||||
// Update the cover.
|
||||
loadCover(manga, presenter.sourceManager.get(manga.source)!!, presenter.coverCache)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the cover of a manga in a image view.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
* @param source the source of the manga.
|
||||
* @param coverCache the cache that stores the cover in the filesystem.
|
||||
*/
|
||||
private fun loadCover(manga: Manga, source: Source, coverCache: CoverCache) {
|
||||
Glide.clear(view.thumbnail)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
coverCache.saveOrLoadFromCache(manga.thumbnail_url, source.glideHeaders) {
|
||||
if (adapter.fragment.isResumed && this.manga == manga) {
|
||||
Glide.with(view.context)
|
||||
.load(it)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.signature(StringSignature(it.lastModified().toString()))
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(itemView.thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
||||
Glide.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(view.thumbnail)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.event
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
@ -11,10 +11,10 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.event.LibraryMangaEvent
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.BehaviorSubject
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@ -236,26 +236,18 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
* Remove the selected manga from the library.
|
||||
*/
|
||||
fun deleteMangas() {
|
||||
for (manga in selectedMangas) {
|
||||
manga.favorite = false
|
||||
}
|
||||
// Create a set of the list
|
||||
val mangaToDelete = selectedMangas.toSet()
|
||||
|
||||
db.insertMangas(selectedMangas).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given list of manga to categories.
|
||||
*
|
||||
* @param positions the indexes of the selected categories.
|
||||
* @param mangas the list of manga to move.
|
||||
*/
|
||||
fun moveMangasToCategories(positions: Array<Int>, mangas: List<Manga>) {
|
||||
val categoriesToAdd = ArrayList<Category>()
|
||||
for (index in positions) {
|
||||
categoriesToAdd.add(categories[index])
|
||||
}
|
||||
|
||||
moveMangasToCategories(categoriesToAdd, mangas)
|
||||
Observable.from(mangaToDelete)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnNext {
|
||||
it.favorite = false
|
||||
coverCache.deleteFromCache(it.thumbnail_url)
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.insertMangas(it).asRxObservable() }
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,7 +8,7 @@ import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.app.FragmentPagerAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.event.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.event
|
||||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
@ -4,8 +4,8 @@ import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.event.ChapterCountEvent
|
||||
import eu.kanade.tachiyomi.event.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import rx.Observable
|
||||
|
@ -11,8 +11,8 @@ import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
import eu.kanade.tachiyomi.event.ChapterCountEvent
|
||||
import eu.kanade.tachiyomi.event.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import rx.Observable
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.event
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import rx.Observable
|
||||
import rx.subjects.BehaviorSubject
|
@ -6,8 +6,6 @@ import android.os.Bundle
|
||||
import android.view.*
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.signature.StringSignature
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
@ -112,45 +110,19 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
||||
// Set the favorite drawable to the correct one.
|
||||
setFavoriteDrawable(manga.favorite)
|
||||
|
||||
// Initialize CoverCache and Glide headers to retrieve cover information.
|
||||
val coverCache = presenter.coverCache
|
||||
val headers = presenter.source.glideHeaders
|
||||
|
||||
// Set cover if it wasn't already.
|
||||
if (manga_cover.drawable == null) {
|
||||
manga.thumbnail_url?.let { url ->
|
||||
if (manga.favorite) {
|
||||
coverCache.saveOrLoadFromCache(url, headers) {
|
||||
if (isResumed) {
|
||||
Glide.with(context)
|
||||
.load(it)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.signature(StringSignature(it.lastModified().toString()))
|
||||
.into(manga_cover)
|
||||
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.into(manga_cover)
|
||||
|
||||
Glide.with(context)
|
||||
.load(it)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.signature(StringSignature(it.lastModified().toString()))
|
||||
.into(backdrop)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Glide.with(context)
|
||||
.load(if (headers != null) GlideUrl(url, headers) else url)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.centerCrop()
|
||||
.into(manga_cover)
|
||||
|
||||
Glide.with(context)
|
||||
.load(if (headers != null) GlideUrl(url, headers) else url)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.centerCrop()
|
||||
.into(backdrop)
|
||||
}
|
||||
}
|
||||
Glide.with(context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.into(backdrop)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,8 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
import eu.kanade.tachiyomi.event.ChapterCountEvent
|
||||
import eu.kanade.tachiyomi.event.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@ -116,22 +115,11 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
||||
*/
|
||||
fun toggleFavorite() {
|
||||
manga.favorite = !manga.favorite
|
||||
onMangaFavoriteChange(manga.favorite)
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
refreshManga()
|
||||
}
|
||||
|
||||
/**
|
||||
* (Removes / Saves) cover depending on favorite status.
|
||||
*
|
||||
* @param isFavorite determines if manga is favorite or not.
|
||||
*/
|
||||
private fun onMangaFavoriteChange(isFavorite: Boolean) {
|
||||
if (isFavorite) {
|
||||
coverCache.save(manga.thumbnail_url, source.glideHeaders)
|
||||
} else {
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
refreshManga()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.event.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
|
@ -21,7 +21,7 @@ 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.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderEvent
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.ui.base.listener.SimpleAnimationListener
|
||||
import eu.kanade.tachiyomi.ui.base.listener.SimpleSeekBarListener
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.event
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderEvent
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import rx.Observable
|
||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
@ -20,6 +21,7 @@ class SettingsActivity : BaseActivity() {
|
||||
@Inject lateinit var db: DatabaseHelper
|
||||
@Inject lateinit var sourceManager: SourceManager
|
||||
@Inject lateinit var syncManager: MangaSyncManager
|
||||
@Inject lateinit var networkHelper: NetworkHelper
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
setAppTheme()
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.Preference
|
||||
import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
@ -16,8 +15,6 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||
|
||||
private var clearCacheSubscription: Subscription? = null
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(resourcePreference: Int, resourceTitle: Int): SettingsNestedFragment {
|
||||
@ -27,17 +24,28 @@ class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
|
||||
val clearCache = findPreference(getString(R.string.pref_clear_chapter_cache_key))
|
||||
val clearDatabase = findPreference(getString(R.string.pref_clear_database_key))
|
||||
private val clearCache by lazy { findPreference(getString(R.string.pref_clear_chapter_cache_key)) }
|
||||
|
||||
clearCache.setOnPreferenceClickListener { preference ->
|
||||
clearChapterCache(preference)
|
||||
private val clearDatabase by lazy { findPreference(getString(R.string.pref_clear_database_key)) }
|
||||
|
||||
private val clearCookies by lazy { findPreference(getString(R.string.pref_clear_cookies_key)) }
|
||||
|
||||
private var clearCacheSubscription: Subscription? = null
|
||||
|
||||
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
|
||||
clearCache.setOnPreferenceClickListener {
|
||||
clearChapterCache()
|
||||
true
|
||||
}
|
||||
clearCache.summary = getString(R.string.used_cache, chapterCache.readableSize)
|
||||
|
||||
clearDatabase.setOnPreferenceClickListener { preference ->
|
||||
clearCookies.setOnPreferenceClickListener {
|
||||
settingsActivity.networkHelper.cookies.removeAll()
|
||||
activity.toast(R.string.cookies_cleared)
|
||||
true
|
||||
}
|
||||
|
||||
clearDatabase.setOnPreferenceClickListener {
|
||||
clearDatabase()
|
||||
true
|
||||
}
|
||||
@ -48,7 +56,7 @@ class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun clearChapterCache(preference: Preference) {
|
||||
private fun clearChapterCache() {
|
||||
val deletedFiles = AtomicInteger()
|
||||
|
||||
val files = chapterCache.cacheDir.listFiles()
|
||||
@ -78,7 +86,7 @@ class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||
}, {
|
||||
dialog.dismiss()
|
||||
activity.toast(getString(R.string.cache_deleted, deletedFiles.get()))
|
||||
preference.summary = getString(R.string.used_cache, chapterCache.readableSize)
|
||||
clearCache.summary = getString(R.string.used_cache, chapterCache.readableSize)
|
||||
})
|
||||
}
|
||||
|
||||
@ -87,7 +95,10 @@ class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||
.content(R.string.clear_database_confirmation)
|
||||
.positiveText(android.R.string.yes)
|
||||
.negativeText(android.R.string.no)
|
||||
.onPositive { dialog, which -> db.deleteMangasNotInLibrary().executeAsBlocking() }
|
||||
.onPositive { dialog, which ->
|
||||
db.deleteMangasNotInLibrary().executeAsBlocking()
|
||||
activity.toast(R.string.clear_database_completed)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,7 @@
|
||||
|
||||
<string name="pref_clear_chapter_cache_key">pref_clear_chapter_cache_key</string>
|
||||
<string name="pref_clear_database_key">pref_clear_database_key</string>
|
||||
<string name="pref_clear_cookies_key">pref_clear_cookies_key</string>
|
||||
|
||||
<string name="pref_version">pref_version</string>
|
||||
<string name="pref_build_time">pref_build_time</string>
|
||||
|
@ -158,9 +158,12 @@
|
||||
<string name="used_cache">Used: %1$s</string>
|
||||
<string name="cache_deleted">Cache cleared. %1$d files have been deleted</string>
|
||||
<string name="cache_delete_error">An error occurred while clearing cache</string>
|
||||
<string name="pref_clear_cookies">Clear cookies</string>
|
||||
<string name="cookies_cleared">Cookies cleared</string>
|
||||
<string name="pref_clear_database">Clear database</string>
|
||||
<string name="pref_clear_database_summary">Delete manga and chapters that are not in your library</string>
|
||||
<string name="clear_database_confirmation">Are you sure? Read chapters and progress of non-library manga will be lost</string>
|
||||
<string name="clear_database_completed">Entries deleted</string>
|
||||
<string name="pref_show_warning_message">Show warnings</string>
|
||||
<string name="pref_show_warning_message_summary">Show warning messages during library sync </string>
|
||||
<string name="pref_reencode">Reencode images</string>
|
||||
|
@ -6,6 +6,10 @@
|
||||
android:key="@string/pref_clear_chapter_cache_key"
|
||||
android:title="@string/pref_clear_chapter_cache"/>
|
||||
|
||||
<Preference
|
||||
android:key="@string/pref_clear_cookies_key"
|
||||
android:title="@string/pref_clear_cookies"/>
|
||||
|
||||
<Preference
|
||||
android:key="@string/pref_clear_database_key"
|
||||
android:summary="@string/pref_clear_database_summary"
|
||||
|
Loading…
Reference in New Issue
Block a user