Browse Source

Kissmanga loading through Cloudflare. A lot of refactoring was needed

len 9 years ago
parent
commit
6e8a41f898
42 changed files with 755 additions and 526 deletions
  1. 4 1
      app/build.gradle
  2. 1 1
      app/src/main/AndroidManifest.xml
  3. 6 71
      app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
  4. 0 22
      app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt
  5. 34 0
      app/src/main/java/eu/kanade/tachiyomi/data/glide/AppGlideModule.kt
  6. 67 0
      app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaDataFetcher.kt
  7. 118 0
      app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt
  8. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/mangasync/services/MyAnimeList.kt
  9. 81 0
      app/src/main/java/eu/kanade/tachiyomi/data/network/CloudflareScraper.kt
  10. 26 29
      app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt
  11. 19 0
      app/src/main/java/eu/kanade/tachiyomi/data/network/PersistentCookieJar.kt
  12. 75 0
      app/src/main/java/eu/kanade/tachiyomi/data/network/PersistentCookieStore.kt
  13. 10 18
      app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.kt
  14. 3 3
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.java
  15. 0 234
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.java
  16. 200 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt
  17. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/ReadMangaToday.java
  18. 5 0
      app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.kt
  19. 2 7
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt
  20. 9 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
  21. 1 2
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
  22. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt
  23. 6 3
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt
  24. 11 31
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
  25. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt
  26. 13 21
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  27. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt
  28. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt
  29. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
  30. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt
  31. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt
  32. 12 40
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt
  33. 4 16
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt
  34. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.kt
  35. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
  36. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderEvent.kt
  37. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
  38. 2 0
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt
  39. 23 12
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt
  40. 1 0
      app/src/main/res/values/keys.xml
  41. 3 0
      app/src/main/res/values/strings.xml
  42. 4 0
      app/src/main/res/xml/pref_advanced.xml

+ 4 - 1
app/build.gradle

@@ -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'

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -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>

+ 6 - 71
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt

@@ -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()
     }
 

+ 0 - 22
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt

@@ -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!
-    }
-}

+ 34 - 0
app/src/main/java/eu/kanade/tachiyomi/data/glide/AppGlideModule.kt

@@ -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())
+    }
+}

+ 67 - 0
app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaDataFetcher.kt

@@ -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()
+    }
+
+}

+ 118 - 0
app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt

@@ -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()
+        }
+    }
+
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/mangasync/services/MyAnimeList.kt

@@ -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 {

+ 81 - 0
app/src/main/java/eu/kanade/tachiyomi/data/network/CloudflareScraper.kt

@@ -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()
+        }
+    }
+
+}

+ 26 - 29
app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt

@@ -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 forceCacheInterceptor = { chain: Interceptor.Chain ->
-        val originalResponse = chain.proceed(chain.request())
-        originalResponse.newBuilder()
-                .removeHeader("Pragma")
-                .header("Cache-Control", "max-age=" + 600)
-                .build()
-    }
+    private val cookieManager = PersistentCookieJar(context)
 
-    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()
     }
 
-
 }

+ 19 - 0
app/src/main/java/eu/kanade/tachiyomi/data/network/PersistentCookieJar.kt

@@ -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)
+    }
+}

+ 75 - 0
app/src/main/java/eu/kanade/tachiyomi/data/network/PersistentCookieStore.kt

@@ -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()
+
+}

+ 10 - 18
app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.kt

@@ -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
-    }
-
 }

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.java

@@ -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;
             }
 

+ 0 - 234
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.java

@@ -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;
-    }
-
-}

+ 200 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt

@@ -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"
+    }
+
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/ReadMangaToday.java

@@ -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) {

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.kt

@@ -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
 

+ 2 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt

@@ -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)
         }
     }
 }

+ 9 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt

@@ -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()
     }
 

+ 1 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt

@@ -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)
     }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt

@@ -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

+ 6 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt

@@ -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
                 }

+ 11 - 31
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt

@@ -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 - 1
app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangaEvent.kt → app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt

@@ -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

+ 13 - 21
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -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
-        }
-
-        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)
+        // Create a set of the list
+        val mangaToDelete = selectedMangas.toSet()
+
+        Observable.from(mangaToDelete)
+                .subscribeOn(Schedulers.io())
+                .doOnNext {
+                    it.favorite = false
+                    coverCache.deleteFromCache(it.thumbnail_url)
+                }
+                .toList()
+                .flatMap { db.insertMangas(it).asRxObservable() }
+                .subscribe()
     }
 
     /**

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt

@@ -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 - 1
app/src/main/java/eu/kanade/tachiyomi/event/MangaEvent.kt → app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.event
+package eu.kanade.tachiyomi.ui.manga
 
 import eu.kanade.tachiyomi.data.database.models.Manga
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

@@ -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

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt

@@ -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 - 1
app/src/main/java/eu/kanade/tachiyomi/event/ChapterCountEvent.kt → app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.event
+package eu.kanade.tachiyomi.ui.manga.info
 
 import rx.Observable
 import rx.subjects.BehaviorSubject

+ 12 - 40
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt

@@ -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)
-
-                            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)
-                }
-            }
+        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(manga)
+                    .diskCacheStrategy(DiskCacheStrategy.RESULT)
+                    .centerCrop()
+                    .into(backdrop)
         }
     }
 

+ 4 - 16
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt

@@ -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()
     }
 
     /**

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.kt

@@ -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

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -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 - 1
app/src/main/java/eu/kanade/tachiyomi/event/ReaderEvent.kt → app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderEvent.kt

@@ -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

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -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

+ 2 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt

@@ -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()

+ 23 - 12
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt

@@ -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)) }
+
+    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
 
-        clearCache.setOnPreferenceClickListener { preference ->
-            clearChapterCache(preference)
+    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()
     }
 

+ 1 - 0
app/src/main/res/values/keys.xml

@@ -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>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -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>

+ 4 - 0
app/src/main/res/xml/pref_advanced.xml

@@ -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"