mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Kissmanga loading through Cloudflare. A lot of refactoring was needed
This commit is contained in:
		@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user