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