Kissmanga loading through Cloudflare. A lot of refactoring was needed

This commit is contained in:
len 2016-05-10 15:09:44 +02:00
parent 8da11dbdb9
commit 6e8a41f898
42 changed files with 753 additions and 524 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
val originalResponse = chain.proceed(chain.request()) .cookieJar(cookieManager)
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build()
}
private val client = OkHttpClient.Builder()
.cookieJar(JavaNetCookieJar(cookieManager))
.cache(Cache(cacheDir, cacheSize)) .cache(Cache(cacheDir, cacheSize))
.build() .build()
private val forceCacheClient = client.newBuilder() val forceCacheClient = defaultClient.newBuilder()
.addNetworkInterceptor(forceCacheInterceptor) .addNetworkInterceptor({ chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build()
})
.build() .build()
val cookies: CookieStore val cloudflareClient = defaultClient.newBuilder()
get() = cookieManager.cookieStore .addInterceptor { CloudflareScraper.request(it, cookies) }
.build()
val cookies: PersistentCookieStore
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()
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()) { Glide.with(view.context)
coverCache.saveOrLoadFromCache(manga.thumbnail_url, source.glideHeaders) { .load(manga)
if (adapter.fragment.isResumed && this.manga == manga) { .diskCacheStrategy(DiskCacheStrategy.RESULT)
Glide.with(view.context) .centerCrop()
.load(it) .placeholder(android.R.color.transparent)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .into(view.thumbnail)
.centerCrop()
.signature(StringSignature(it.lastModified().toString()))
.placeholder(android.R.color.transparent)
.into(itemView.thumbnail)
}
}
}
} }
} }

View File

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

View File

@ -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
* Move the given list of manga to categories. coverCache.deleteFromCache(it.thumbnail_url)
* }
* @param positions the indexes of the selected categories. .toList()
* @param mangas the list of manga to move. .flatMap { db.insertMangas(it).asRxObservable() }
*/ .subscribe()
fun moveMangasToCategories(positions: Array<Int>, mangas: List<Manga>) {
val categoriesToAdd = ArrayList<Category>()
for (index in positions) {
categoriesToAdd.add(categories[index])
}
moveMangasToCategories(categoriesToAdd, mangas)
} }
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,45 +110,19 @@ 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 -> Glide.with(context)
if (manga.favorite) { .load(manga)
coverCache.saveOrLoadFromCache(url, headers) { .diskCacheStrategy(DiskCacheStrategy.RESULT)
if (isResumed) { .centerCrop()
Glide.with(context) .into(manga_cover)
.load(it)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.signature(StringSignature(it.lastModified().toString()))
.into(manga_cover)
Glide.with(context) Glide.with(context)
.load(it) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop() .centerCrop()
.signature(StringSignature(it.lastModified().toString())) .into(backdrop)
.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)
}
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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