mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-10 21:57:47 +01:00
Kissmanga loading through Cloudflare. A lot of refactoring was needed
This commit is contained in:
parent
8da11dbdb9
commit
6e8a41f898
42 changed files with 753 additions and 524 deletions
|
@ -118,7 +118,6 @@ dependencies {
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
|
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
|
||||||
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
|
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
||||||
|
@ -131,6 +130,9 @@ dependencies {
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.6.2'
|
compile 'com.google.code.gson:gson:2.6.2'
|
||||||
|
|
||||||
|
// JavaScript engine
|
||||||
|
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
||||||
|
|
||||||
// Disk cache
|
// Disk cache
|
||||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||||
|
|
||||||
|
@ -154,6 +156,7 @@ dependencies {
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||||
|
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.1.2'
|
compile 'com.jakewharton.timber:timber:4.1.2'
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
|
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||||
android:value="GlideModule" />
|
android:value="GlideModule" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
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 eu.kanade.tachiyomi.util.DiskUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -27,80 +22,19 @@ class CoverCache(private val context: Context) {
|
||||||
*/
|
*/
|
||||||
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
|
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.
|
* Returns the cover from cache.
|
||||||
|
*
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param thumbnailUrl the thumbnail url.
|
||||||
* @return cover image.
|
* @return cover image.
|
||||||
*/
|
*/
|
||||||
private fun getCoverFromCache(thumbnailUrl: String): File {
|
fun getCoverFile(thumbnailUrl: String): File {
|
||||||
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
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.
|
* Copy the given stream to this cache.
|
||||||
|
*
|
||||||
* @param thumbnailUrl url of the thumbnail.
|
* @param thumbnailUrl url of the thumbnail.
|
||||||
* @param inputStream the stream to copy.
|
* @param inputStream the stream to copy.
|
||||||
* @throws IOException if there's any error.
|
* @throws IOException if there's any error.
|
||||||
|
@ -108,13 +42,14 @@ class CoverCache(private val context: Context) {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
||||||
// Get destination file.
|
// Get destination file.
|
||||||
val destFile = getCoverFromCache(thumbnailUrl)
|
val destFile = getCoverFile(thumbnailUrl)
|
||||||
|
|
||||||
destFile.outputStream().use { inputStream.copyTo(it) }
|
destFile.outputStream().use { inputStream.copyTo(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the cover file from the cache.
|
* Delete the cover file from the cache.
|
||||||
|
*
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param thumbnailUrl the thumbnail url.
|
||||||
* @return status of deletion.
|
* @return status of deletion.
|
||||||
*/
|
*/
|
||||||
|
@ -124,7 +59,7 @@ class CoverCache(private val context: Context) {
|
||||||
return false
|
return false
|
||||||
|
|
||||||
// Remove file.
|
// Remove file.
|
||||||
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
val file = getCoverFile(thumbnailUrl!!)
|
||||||
return file.exists() && file.delete()
|
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
|
// MAL doesn't support score with decimals
|
||||||
fun getList(): Observable<List<MangaSync>> {
|
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) }
|
.map { Jsoup.parse(it) }
|
||||||
.flatMap { Observable.from(it.select("manga")) }
|
.flatMap { Observable.from(it.select("manga")) }
|
||||||
.map {
|
.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
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import okhttp3.*
|
import okhttp3.Cache
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.CookieManager
|
|
||||||
import java.net.CookiePolicy
|
|
||||||
import java.net.CookieStore
|
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
|
@ -14,43 +14,41 @@ class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
private val cookieManager = CookieManager().apply {
|
private val cookieManager = PersistentCookieJar(context)
|
||||||
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val forceCacheInterceptor = { chain: Interceptor.Chain ->
|
val defaultClient = OkHttpClient.Builder()
|
||||||
|
.cookieJar(cookieManager)
|
||||||
|
.cache(Cache(cacheDir, cacheSize))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val forceCacheClient = defaultClient.newBuilder()
|
||||||
|
.addNetworkInterceptor({ chain ->
|
||||||
val originalResponse = chain.proceed(chain.request())
|
val originalResponse = chain.proceed(chain.request())
|
||||||
originalResponse.newBuilder()
|
originalResponse.newBuilder()
|
||||||
.removeHeader("Pragma")
|
.removeHeader("Pragma")
|
||||||
.header("Cache-Control", "max-age=" + 600)
|
.header("Cache-Control", "max-age=" + 600)
|
||||||
.build()
|
.build()
|
||||||
}
|
})
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
|
||||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
|
||||||
.cache(Cache(cacheDir, cacheSize))
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val forceCacheClient = client.newBuilder()
|
val cloudflareClient = defaultClient.newBuilder()
|
||||||
.addNetworkInterceptor(forceCacheInterceptor)
|
.addInterceptor { CloudflareScraper.request(it, cookies) }
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cookies: CookieStore
|
val cookies: PersistentCookieStore
|
||||||
get() = cookieManager.cookieStore
|
get() = cookieManager.store
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
|
fun request(request: Request, client: OkHttpClient = defaultClient): Observable<Response> {
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable {
|
||||||
val c = if (forceCache) forceCacheClient else client
|
client.newCall(request).execute().apply { body().close() }
|
||||||
c.newCall(request).execute().apply { body().close() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
|
fun requestBody(request: Request, client: OkHttpClient = defaultClient): Observable<String> {
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable {
|
||||||
val c = if (forceCache) forceCacheClient else client
|
client.newCall(request).execute().body().string()
|
||||||
c.newCall(request).execute().body().string()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +57,7 @@ class NetworkHelper(context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
|
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
|
||||||
val progressClient = client.newBuilder()
|
val progressClient = defaultClient.newBuilder()
|
||||||
.cache(null)
|
.cache(null)
|
||||||
.addNetworkInterceptor { chain ->
|
.addNetworkInterceptor { chain ->
|
||||||
val originalResponse = chain.proceed(chain.request())
|
val originalResponse = chain.proceed(chain.request())
|
||||||
|
@ -72,5 +70,4 @@ class NetworkHelper(context: Context) {
|
||||||
return progressClient.newCall(request).execute()
|
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
|
package eu.kanade.tachiyomi.data.source.base
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.bumptech.glide.load.model.LazyHeaders
|
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
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.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
@ -27,12 +27,13 @@ abstract class Source(context: Context) : BaseSource() {
|
||||||
|
|
||||||
val requestHeaders by lazy { headersBuilder().build() }
|
val requestHeaders by lazy { headersBuilder().build() }
|
||||||
|
|
||||||
val glideHeaders by lazy { glideHeadersBuilder().build() }
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
App.get(context).component.inject(this)
|
App.get(context).component.inject(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open val networkClient: OkHttpClient
|
||||||
|
get() = networkService.defaultClient
|
||||||
|
|
||||||
override fun isLoginRequired(): Boolean {
|
override fun isLoginRequired(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -75,7 +76,7 @@ abstract class Source(context: Context) : BaseSource() {
|
||||||
|
|
||||||
// Get the most popular mangas from the source
|
// Get the most popular mangas from the source
|
||||||
open fun pullPopularMangasFromNetwork(page: MangasPage): Observable<MangasPage> {
|
open fun pullPopularMangasFromNetwork(page: MangasPage): Observable<MangasPage> {
|
||||||
return networkService.requestBody(popularMangaRequest(page), true)
|
return networkService.requestBody(popularMangaRequest(page), networkClient)
|
||||||
.map { Jsoup.parse(it) }
|
.map { Jsoup.parse(it) }
|
||||||
.doOnNext { doc -> page.mangas = parsePopularMangasFromHtml(doc) }
|
.doOnNext { doc -> page.mangas = parsePopularMangasFromHtml(doc) }
|
||||||
.doOnNext { doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page) }
|
.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
|
// Get mangas from the source with a query
|
||||||
open fun searchMangasFromNetwork(page: MangasPage, query: String): Observable<MangasPage> {
|
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) }
|
.map { Jsoup.parse(it) }
|
||||||
.doOnNext { doc -> page.mangas = parseSearchFromHtml(doc) }
|
.doOnNext { doc -> page.mangas = parseSearchFromHtml(doc) }
|
||||||
.doOnNext { doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query) }
|
.doOnNext { doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query) }
|
||||||
|
@ -93,13 +94,13 @@ abstract class Source(context: Context) : BaseSource() {
|
||||||
|
|
||||||
// Get manga details from the source
|
// Get manga details from the source
|
||||||
open fun pullMangaFromNetwork(mangaUrl: String): Observable<Manga> {
|
open fun pullMangaFromNetwork(mangaUrl: String): Observable<Manga> {
|
||||||
return networkService.requestBody(mangaDetailsRequest(mangaUrl))
|
return networkService.requestBody(mangaDetailsRequest(mangaUrl), networkClient)
|
||||||
.flatMap { Observable.just(parseHtmlToManga(mangaUrl, it)) }
|
.flatMap { Observable.just(parseHtmlToManga(mangaUrl, it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get chapter list of a manga from the source
|
// Get chapter list of a manga from the source
|
||||||
open fun pullChaptersFromNetwork(mangaUrl: String): Observable<List<Chapter>> {
|
open fun pullChaptersFromNetwork(mangaUrl: String): Observable<List<Chapter>> {
|
||||||
return networkService.requestBody(chapterListRequest(mangaUrl))
|
return networkService.requestBody(chapterListRequest(mangaUrl), networkClient)
|
||||||
.flatMap { unparsedHtml ->
|
.flatMap { unparsedHtml ->
|
||||||
val chapters = parseHtmlToChapters(unparsedHtml)
|
val chapters = parseHtmlToChapters(unparsedHtml)
|
||||||
if (!chapters.isEmpty())
|
if (!chapters.isEmpty())
|
||||||
|
@ -116,7 +117,7 @@ abstract class Source(context: Context) : BaseSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun pullPageListFromNetwork(chapterUrl: String): Observable<List<Page>> {
|
open fun pullPageListFromNetwork(chapterUrl: String): Observable<List<Page>> {
|
||||||
return networkService.requestBody(pageListRequest(chapterUrl))
|
return networkService.requestBody(pageListRequest(chapterUrl), networkClient)
|
||||||
.flatMap { unparsedHtml ->
|
.flatMap { unparsedHtml ->
|
||||||
val pages = convertToPages(parseHtmlToPageUrls(unparsedHtml))
|
val pages = convertToPages(parseHtmlToPageUrls(unparsedHtml))
|
||||||
if (!pages.isEmpty())
|
if (!pages.isEmpty())
|
||||||
|
@ -141,7 +142,7 @@ abstract class Source(context: Context) : BaseSource() {
|
||||||
|
|
||||||
open fun getImageUrlFromPage(page: Page): Observable<Page> {
|
open fun getImageUrlFromPage(page: Page): Observable<Page> {
|
||||||
page.status = Page.LOAD_PAGE
|
page.status = Page.LOAD_PAGE
|
||||||
return networkService.requestBody(imageUrlRequest(page))
|
return networkService.requestBody(imageUrlRequest(page), networkClient)
|
||||||
.flatMap { unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)) }
|
.flatMap { unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)) }
|
||||||
.onErrorResumeNext { e ->
|
.onErrorResumeNext { e ->
|
||||||
page.status = Page.ERROR
|
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.nodes.Element;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
|
|
||||||
import java.net.HttpCookie;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.text.ParseException;
|
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.MangasPage;
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
import eu.kanade.tachiyomi.util.Parser;
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
import okhttp3.Cookie;
|
||||||
import okhttp3.FormBody;
|
import okhttp3.FormBody;
|
||||||
import okhttp3.Headers;
|
import okhttp3.Headers;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
|
@ -358,8 +358,8 @@ public class Batoto extends LoginSource {
|
||||||
@Override
|
@Override
|
||||||
public boolean isLogged() {
|
public boolean isLogged() {
|
||||||
try {
|
try {
|
||||||
for ( HttpCookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL)) ) {
|
for (Cookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL))) {
|
||||||
if (cookie.getName().equals("pass_hash"))
|
if (cookie.name().equals("pass_hash"))
|
||||||
return true;
|
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
|
@Override
|
||||||
public Observable<MangasPage> searchMangasFromNetwork(final MangasPage page, String query) {
|
public Observable<MangasPage> searchMangasFromNetwork(final MangasPage page, String query) {
|
||||||
return networkService
|
return networkService
|
||||||
.requestBody(searchMangaRequest(page, query), true)
|
.requestBody(searchMangaRequest(page, query), networkService.getDefaultClient())
|
||||||
.doOnNext(new Action1<String>() {
|
.doOnNext(new Action1<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String doc) {
|
public void call(String doc) {
|
||||||
|
|
|
@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.injection.component
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
|
import eu.kanade.tachiyomi.data.glide.AppGlideModule
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
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.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
||||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||||
|
@ -51,6 +53,9 @@ interface AppComponent {
|
||||||
fun inject(downloadService: DownloadService)
|
fun inject(downloadService: DownloadService)
|
||||||
fun inject(updateMangaSyncService: UpdateMangaSyncService)
|
fun inject(updateMangaSyncService: UpdateMangaSyncService)
|
||||||
|
|
||||||
|
fun inject(mangaModelLoader: MangaModelLoader)
|
||||||
|
fun inject(appGlideModule: AppGlideModule)
|
||||||
|
|
||||||
fun inject(updateDownloader: UpdateDownloader)
|
fun inject(updateDownloader: UpdateDownloader)
|
||||||
fun application(): Application
|
fun application(): Application
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.catalogue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
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.
|
* @param manga the manga to bind.
|
||||||
*/
|
*/
|
||||||
fun setImage(manga: Manga) {
|
fun setImage(manga: Manga) {
|
||||||
|
Glide.clear(view.thumbnail)
|
||||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||||
val url = manga.thumbnail_url!!
|
|
||||||
val headers = adapter.fragment.presenter.source.glideHeaders
|
|
||||||
|
|
||||||
Glide.with(view.context)
|
Glide.with(view.context)
|
||||||
.load(if (headers != null) GlideUrl(url, headers) else url)
|
.load(manga)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.placeholder(android.R.color.transparent)
|
.placeholder(android.R.color.transparent)
|
||||||
.into(view.thumbnail)
|
.into(view.thumbnail)
|
||||||
|
|
||||||
} else {
|
|
||||||
Glide.clear(view.thumbnail)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.ui.catalogue
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
@ -38,6 +39,11 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var prefs: PreferencesHelper
|
@Inject lateinit var prefs: PreferencesHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cover cache.
|
||||||
|
*/
|
||||||
|
@Inject lateinit var coverCache: CoverCache
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enabled sources.
|
* Enabled sources.
|
||||||
*/
|
*/
|
||||||
|
@ -335,6 +341,9 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||||
*/
|
*/
|
||||||
fun changeMangaFavorite(manga: Manga) {
|
fun changeMangaFavorite(manga: Manga) {
|
||||||
manga.favorite = !manga.favorite
|
manga.favorite = !manga.favorite
|
||||||
|
if (!manga.favorite) {
|
||||||
|
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||||
|
}
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,10 +98,9 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
|
||||||
* @param position the position to bind.
|
* @param position the position to bind.
|
||||||
*/
|
*/
|
||||||
override fun onBindViewHolder(holder: LibraryHolder, position: Int) {
|
override fun onBindViewHolder(holder: LibraryHolder, position: Int) {
|
||||||
val presenter = (fragment.parentFragment as LibraryFragment).presenter
|
|
||||||
val manga = getItem(position)
|
val manga = getItem(position)
|
||||||
|
|
||||||
holder.onSetValues(manga, presenter)
|
holder.onSetValues(manga)
|
||||||
//When user scrolls this bind the correct selection status
|
//When user scrolls this bind the correct selection status
|
||||||
holder.itemView.isActivated = isSelected(position)
|
holder.itemView.isActivated = isSelected(position)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
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.adapter.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
|
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
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.base.fragment.BaseRxFragment
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryActivity
|
import eu.kanade.tachiyomi.ui.category.CategoryActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
@ -388,7 +387,10 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||||
* @param mangas the manga list to move.
|
* @param mangas the manga list to move.
|
||||||
*/
|
*/
|
||||||
private fun moveMangasToCategories(mangas: List<Manga>) {
|
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)
|
val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
|
||||||
.map { categories.indexOf(it) }
|
.map { categories.indexOf(it) }
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
|
@ -397,7 +399,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||||
.title(R.string.action_move_category)
|
.title(R.string.action_move_category)
|
||||||
.items(categories.map { it.name })
|
.items(categories.map { it.name })
|
||||||
.itemsCallbackMultiChoice(commonCategoriesIndexes) { dialog, positions, text ->
|
.itemsCallbackMultiChoice(commonCategoriesIndexes) { dialog, positions, text ->
|
||||||
presenter.moveMangasToCategories(positions, mangas)
|
val selectedCategories = positions.map { categories[it] }
|
||||||
|
presenter.moveMangasToCategories(selectedCategories, mangas)
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,7 @@ package eu.kanade.tachiyomi.ui.library
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
|
||||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
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.
|
* @param listener a listener to react to single tap and long tap events.
|
||||||
* @constructor creates a new library holder.
|
* @constructor creates a new library holder.
|
||||||
*/
|
*/
|
||||||
class LibraryHolder(private val view: View, private val adapter: LibraryCategoryAdapter, listener: FlexibleViewHolder.OnListItemClickListener) :
|
class LibraryHolder(private val view: View,
|
||||||
FlexibleViewHolder(view, adapter, listener) {
|
private val adapter: LibraryCategoryAdapter,
|
||||||
|
listener: FlexibleViewHolder.OnListItemClickListener)
|
||||||
|
: FlexibleViewHolder(view, adapter, listener) {
|
||||||
|
|
||||||
private var manga: Manga? = null
|
private var manga: Manga? = null
|
||||||
|
|
||||||
|
@ -29,9 +28,8 @@ class LibraryHolder(private val view: View, private val adapter: LibraryCategory
|
||||||
* holder with the given manga.
|
* holder with the given manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga to bind.
|
* @param manga the manga to bind.
|
||||||
* @param presenter the library presenter.
|
|
||||||
*/
|
*/
|
||||||
fun onSetValues(manga: Manga, presenter: LibraryPresenter) {
|
fun onSetValues(manga: Manga) {
|
||||||
this.manga = manga
|
this.manga = manga
|
||||||
|
|
||||||
// Update the title of the manga.
|
// Update the title of the manga.
|
||||||
|
@ -44,31 +42,13 @@ class LibraryHolder(private val view: View, private val adapter: LibraryCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cover.
|
// 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)
|
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)
|
Glide.with(view.context)
|
||||||
.load(it)
|
.load(manga)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.signature(StringSignature(it.lastModified().toString()))
|
|
||||||
.placeholder(android.R.color.transparent)
|
.placeholder(android.R.color.transparent)
|
||||||
.into(itemView.thumbnail)
|
.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.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.event.LibraryMangaEvent
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
import rx.subjects.BehaviorSubject
|
import rx.subjects.BehaviorSubject
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -236,26 +236,18 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||||
* Remove the selected manga from the library.
|
* Remove the selected manga from the library.
|
||||||
*/
|
*/
|
||||||
fun deleteMangas() {
|
fun deleteMangas() {
|
||||||
for (manga in selectedMangas) {
|
// Create a set of the list
|
||||||
manga.favorite = false
|
val mangaToDelete = selectedMangas.toSet()
|
||||||
}
|
|
||||||
|
|
||||||
db.insertMangas(selectedMangas).executeAsBlocking()
|
Observable.from(mangaToDelete)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.doOnNext {
|
||||||
|
it.favorite = false
|
||||||
|
coverCache.deleteFromCache(it.thumbnail_url)
|
||||||
}
|
}
|
||||||
|
.toList()
|
||||||
/**
|
.flatMap { db.insertMangas(it).asRxObservable() }
|
||||||
* Move the given list of manga to categories.
|
.subscribe()
|
||||||
*
|
|
||||||
* @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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,7 +8,7 @@ import android.support.v4.app.FragmentManager
|
||||||
import android.support.v4.app.FragmentPagerAdapter
|
import android.support.v4.app.FragmentPagerAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.base.activity.BaseRxActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
|
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
|
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.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||||
import eu.kanade.tachiyomi.event.ChapterCountEvent
|
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||||
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.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
import rx.Observable
|
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.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.event.ChapterCountEvent
|
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||||
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.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.event
|
package eu.kanade.tachiyomi.ui.manga.info
|
||||||
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.BehaviorSubject
|
import rx.subjects.BehaviorSubject
|
|
@ -6,8 +6,6 @@ import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
|
@ -112,47 +110,21 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
||||||
// Set the favorite drawable to the correct one.
|
// Set the favorite drawable to the correct one.
|
||||||
setFavoriteDrawable(manga.favorite)
|
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.
|
// Set cover if it wasn't already.
|
||||||
if (manga_cover.drawable == null) {
|
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
|
||||||
manga.thumbnail_url?.let { url ->
|
|
||||||
if (manga.favorite) {
|
|
||||||
coverCache.saveOrLoadFromCache(url, headers) {
|
|
||||||
if (isResumed) {
|
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.load(it)
|
.load(manga)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||||
.centerCrop()
|
.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)
|
.into(manga_cover)
|
||||||
|
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.load(if (headers != null) GlideUrl(url, headers) else url)
|
.load(manga)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(backdrop)
|
.into(backdrop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update chapter count TextView.
|
* Update chapter count TextView.
|
||||||
|
|
|
@ -6,9 +6,8 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
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.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
@ -116,22 +115,11 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
||||||
*/
|
*/
|
||||||
fun toggleFavorite() {
|
fun toggleFavorite() {
|
||||||
manga.favorite = !manga.favorite
|
manga.favorite = !manga.favorite
|
||||||
onMangaFavoriteChange(manga.favorite)
|
if (!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 {
|
|
||||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
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.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
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.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
import eu.kanade.tachiyomi.util.toast
|
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.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
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.activity.BaseRxActivity
|
||||||
import eu.kanade.tachiyomi.ui.base.listener.SimpleAnimationListener
|
import eu.kanade.tachiyomi.ui.base.listener.SimpleAnimationListener
|
||||||
import eu.kanade.tachiyomi.ui.base.listener.SimpleSeekBarListener
|
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.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
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.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
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.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
|
@ -20,6 +21,7 @@ class SettingsActivity : BaseActivity() {
|
||||||
@Inject lateinit var db: DatabaseHelper
|
@Inject lateinit var db: DatabaseHelper
|
||||||
@Inject lateinit var sourceManager: SourceManager
|
@Inject lateinit var sourceManager: SourceManager
|
||||||
@Inject lateinit var syncManager: MangaSyncManager
|
@Inject lateinit var syncManager: MangaSyncManager
|
||||||
|
@Inject lateinit var networkHelper: NetworkHelper
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
setAppTheme()
|
setAppTheme()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.preference.Preference
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -16,8 +15,6 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class SettingsAdvancedFragment : SettingsNestedFragment() {
|
class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||||
|
|
||||||
private var clearCacheSubscription: Subscription? = null
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance(resourcePreference: Int, resourceTitle: Int): SettingsNestedFragment {
|
fun newInstance(resourcePreference: Int, resourceTitle: Int): SettingsNestedFragment {
|
||||||
|
@ -27,17 +24,28 @@ class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
|
private val clearCache by lazy { findPreference(getString(R.string.pref_clear_chapter_cache_key)) }
|
||||||
val clearCache = findPreference(getString(R.string.pref_clear_chapter_cache_key))
|
|
||||||
val clearDatabase = findPreference(getString(R.string.pref_clear_database_key))
|
|
||||||
|
|
||||||
clearCache.setOnPreferenceClickListener { preference ->
|
private val clearDatabase by lazy { findPreference(getString(R.string.pref_clear_database_key)) }
|
||||||
clearChapterCache(preference)
|
|
||||||
|
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
|
true
|
||||||
}
|
}
|
||||||
clearCache.summary = getString(R.string.used_cache, chapterCache.readableSize)
|
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()
|
clearDatabase()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -48,7 +56,7 @@ class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearChapterCache(preference: Preference) {
|
private fun clearChapterCache() {
|
||||||
val deletedFiles = AtomicInteger()
|
val deletedFiles = AtomicInteger()
|
||||||
|
|
||||||
val files = chapterCache.cacheDir.listFiles()
|
val files = chapterCache.cacheDir.listFiles()
|
||||||
|
@ -78,7 +86,7 @@ class SettingsAdvancedFragment : SettingsNestedFragment() {
|
||||||
}, {
|
}, {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
activity.toast(getString(R.string.cache_deleted, deletedFiles.get()))
|
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)
|
.content(R.string.clear_database_confirmation)
|
||||||
.positiveText(android.R.string.yes)
|
.positiveText(android.R.string.yes)
|
||||||
.negativeText(android.R.string.no)
|
.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()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
|
|
||||||
<string name="pref_clear_chapter_cache_key">pref_clear_chapter_cache_key</string>
|
<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_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_version">pref_version</string>
|
||||||
<string name="pref_build_time">pref_build_time</string>
|
<string name="pref_build_time">pref_build_time</string>
|
||||||
|
|
|
@ -158,9 +158,12 @@
|
||||||
<string name="used_cache">Used: %1$s</string>
|
<string name="used_cache">Used: %1$s</string>
|
||||||
<string name="cache_deleted">Cache cleared. %1$d files have been deleted</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="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">Clear database</string>
|
||||||
<string name="pref_clear_database_summary">Delete manga and chapters that are not in your library</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_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">Show warnings</string>
|
||||||
<string name="pref_show_warning_message_summary">Show warning messages during library sync </string>
|
<string name="pref_show_warning_message_summary">Show warning messages during library sync </string>
|
||||||
<string name="pref_reencode">Reencode images</string>
|
<string name="pref_reencode">Reencode images</string>
|
||||||
|
|
|
@ -6,6 +6,10 @@
|
||||||
android:key="@string/pref_clear_chapter_cache_key"
|
android:key="@string/pref_clear_chapter_cache_key"
|
||||||
android:title="@string/pref_clear_chapter_cache"/>
|
android:title="@string/pref_clear_chapter_cache"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/pref_clear_cookies_key"
|
||||||
|
android:title="@string/pref_clear_cookies"/>
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:key="@string/pref_clear_database_key"
|
android:key="@string/pref_clear_database_key"
|
||||||
android:summary="@string/pref_clear_database_summary"
|
android:summary="@string/pref_clear_database_summary"
|
||||||
|
|
Loading…
Reference in a new issue