Glide v4
This commit is contained in:
parent
f45efe2aa8
commit
1470e9d5ca
20 changed files with 351 additions and 292 deletions
|
@ -3,6 +3,7 @@ import java.text.SimpleDateFormat
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
if (file("custom.gradle").exists()) {
|
||||
apply from: "custom.gradle"
|
||||
|
@ -169,10 +170,12 @@ dependencies {
|
|||
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||
|
||||
// Image library
|
||||
compile 'com.github.bumptech.glide:glide:3.8.0'
|
||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
|
||||
compile 'com.github.bumptech.glide:glide:4.1.1'
|
||||
compile 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.1.1'
|
||||
|
||||
// Transformations
|
||||
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
||||
compile 'jp.wasabeef:glide-transformations:3.0.1'
|
||||
|
||||
// Logging
|
||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||
|
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
|
@ -24,6 +24,7 @@
|
|||
# Glide specific rules #
|
||||
# https://github.com/bumptech/glide
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
|
|
|
@ -95,10 +95,6 @@
|
|||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false"/>
|
||||
|
||||
<meta-data
|
||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||
android:value="GlideModule" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -1,27 +1,39 @@
|
|||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.ContentValues.TAG
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.*
|
||||
|
||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
override fun loadData(priority: Priority): InputStream {
|
||||
data = file.inputStream()
|
||||
return data!!
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
|
||||
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
try {
|
||||
data = FileInputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Failed to open file", e)
|
||||
}
|
||||
callback.onLoadFailed(e)
|
||||
return
|
||||
}
|
||||
|
||||
callback.onDataReady(data)
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
data?.let { data ->
|
||||
try {
|
||||
data.close()
|
||||
data?.close()
|
||||
} catch (e: IOException) {
|
||||
// Ignore
|
||||
}
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +41,11 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
|||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun getId(): String {
|
||||
return file.toString()
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
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.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A [DataFetcher] for loading a cover of a library manga.
|
||||
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
|
||||
* and copies the result to the cache.
|
||||
*
|
||||
* @param networkFetcher the network fetcher for this cover.
|
||||
* @param manga the manga of the cover to load.
|
||||
* @param file the file where this cover should be. It may exists or not.
|
||||
*/
|
||||
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val manga: Manga,
|
||||
private val file: File)
|
||||
: FileFetcher(file) {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
if (!file.exists()) {
|
||||
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
|
||||
override fun onDataReady(data: InputStream?) {
|
||||
if (data != null) {
|
||||
val tmpFile = File(file.path + ".tmp")
|
||||
try {
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
data.use { output.use { data.copyTo(output) } }
|
||||
tmpFile.renameTo(file)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
loadFromFile(callback)
|
||||
} else {
|
||||
callback.onLoadFailed(Exception("Null data"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception) {
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
|
||||
})
|
||||
} else {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
networkFetcher.cleanup()
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
super.cancel()
|
||||
networkFetcher.cancel()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
|
||||
open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) {
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
|
@ -1,23 +1,24 @@
|
|||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.util.LruCache
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.*
|
||||
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
/**
|
||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||
* Coupled with [MangaUrlFetcher], this class allows to implement the following flow:
|
||||
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
||||
*
|
||||
* - Check in RAM LRU.
|
||||
* - Check in disk LRU.
|
||||
|
@ -26,7 +27,7 @@ import java.io.InputStream
|
|||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||
|
||||
/**
|
||||
* Cover cache where persistent covers are stored.
|
||||
|
@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Base network loader.
|
||||
* Default network client.
|
||||
*/
|
||||
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
||||
InputStream::class.java, context)
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* 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 lruCache = LruCache<String, Pair<GlideUrl, File>>(100)
|
||||
private val lruCache = LruCache<GlideUrl, File>(100)
|
||||
|
||||
/**
|
||||
* Map where request headers are stored for a source.
|
||||
|
@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||
*/
|
||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
||||
|
||||
override fun build(context: Context, factories: GenericLoaderFactory)
|
||||
= MangaModelLoader(context)
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
|
||||
return MangaModelLoader()
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
||||
override fun handles(model: Manga): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fetcher for the given manga or null if the url is empty.
|
||||
*
|
||||
|
@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||
* @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>? {
|
||||
|
||||
override fun buildLoadData(manga: Manga, width: Int, height: Int,
|
||||
options: Options?): ModelLoader.LoadData<InputStream>? {
|
||||
// Check thumbnail is not null or empty
|
||||
val url = manga.thumbnail_url
|
||||
if (url == null || url.isEmpty()) {
|
||||
|
@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||
|
||||
if (url.startsWith("http")) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
|
||||
// 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) = lruCache.get(url) ?:
|
||||
Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply {
|
||||
lruCache.put(url, this)
|
||||
}
|
||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||
|
||||
// Get the resource fetcher for this request url.
|
||||
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) }
|
||||
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
|
||||
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
|
||||
|
||||
if (!manga.favorite) {
|
||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||
}
|
||||
|
||||
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
|
||||
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
|
||||
|
||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return MangaUrlFetcher(networkFetcher, file, manga)
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
||||
} else {
|
||||
// Get the file from the url, removing the scheme if present.
|
||||
val file = File(url.substringAfter("file://"))
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return MangaFileFetcher(file, manga)
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,4 +132,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
|
||||
val value = get(key)
|
||||
return if (value == null) {
|
||||
val answer = defaultValue()
|
||||
put(key, answer)
|
||||
answer
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.load.Key
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
class MangaSignature(manga: Manga, file: File) : Key {
|
||||
|
||||
private val key = manga.thumbnail_url + file.lastModified()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is MangaSignature) {
|
||||
key == other.key
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return key.hashCode()
|
||||
}
|
||||
|
||||
override fun updateDiskCacheKey(md: MessageDigest) {
|
||||
md.update(key.toByteArray(Key.CHARSET))
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
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.FileNotFoundException
|
||||
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 MangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val file: File,
|
||||
private val manga: Manga)
|
||||
: MangaFileFetcher(file, manga) {
|
||||
|
||||
override fun loadData(priority: Priority): InputStream {
|
||||
if (manga.favorite) {
|
||||
synchronized(file) {
|
||||
if (!file.exists()) {
|
||||
val tmpFile = File(file.path + ".tmp")
|
||||
try {
|
||||
// Retrieve source stream.
|
||||
val input = networkFetcher.loadData(priority)
|
||||
?: throw Exception("Couldn't open source stream")
|
||||
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
input.use { output.use { input.copyTo(output) } }
|
||||
tmpFile.renameTo(file)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.loadData(priority)
|
||||
} else {
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
return networkFetcher.loadData(priority)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
networkFetcher.cancel()
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
networkFetcher.cleanup()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +1,18 @@
|
|||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.GlideModule
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -16,17 +22,20 @@ import java.io.InputStream
|
|||
/**
|
||||
* Class used to update Glide module settings
|
||||
*/
|
||||
class AppGlideModule : GlideModule {
|
||||
@GlideModule
|
||||
class TachiGlideModule : AppGlideModule() {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
// Set the cache size of Glide to 15 MiB
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
||||
builder.setDefaultTransitionOptions(Drawable::class.java,
|
||||
DrawableTransitionOptions.withCrossFade())
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide) {
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||
|
||||
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||
|
||||
|
@ -36,16 +36,15 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
|
|||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
Glide.clear(view.thumbnail)
|
||||
GlideApp.with(view.context).clear(view.thumbnail)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(view.context)
|
||||
GlideApp.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(view.thumbnail, view.progress))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import jp.wasabeef.glide.transformations.CropCircleTransformation
|
||||
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
|
||||
|
||||
/**
|
||||
|
@ -37,13 +36,13 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
|
|||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
Glide.clear(view.thumbnail)
|
||||
GlideApp.with(view.context).clear(view.thumbnail)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(view.context)
|
||||
GlideApp.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.bitmapTransform(CropCircleTransformation(view.context))
|
||||
.circleCrop()
|
||||
.dontAnimate()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.global_search
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.*
|
||||
|
||||
|
@ -28,11 +28,11 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
|
|||
}
|
||||
|
||||
fun setImage(manga: Manga) {
|
||||
Glide.clear(itemView.itemImage)
|
||||
GlideApp.with(itemView.context).clear(itemView.itemImage)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(itemView.context)
|
||||
GlideApp.with(itemView.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||
|
||||
/**
|
||||
|
@ -38,10 +38,10 @@ class LibraryGridHolder(
|
|||
}
|
||||
|
||||
// Update the cover.
|
||||
Glide.clear(view.thumbnail)
|
||||
Glide.with(view.context)
|
||||
GlideApp.with(view.context).clear(view.thumbnail)
|
||||
GlideApp.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(view.thumbnail)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import jp.wasabeef.glide.transformations.CropCircleTransformation
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
|
||||
|
||||
/**
|
||||
|
@ -46,12 +45,12 @@ class LibraryListHolder(
|
|||
}
|
||||
|
||||
// Update the cover.
|
||||
Glide.clear(itemView.thumbnail)
|
||||
Glide.with(itemView.context)
|
||||
GlideApp.with(itemView.context).clear(itemView.thumbnail)
|
||||
GlideApp.with(itemView.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.bitmapTransform(CropCircleTransformation(itemView.context))
|
||||
.circleCrop()
|
||||
.dontAnimate()
|
||||
.into(itemView.thumbnail)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import android.app.Dialog
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -12,20 +14,22 @@ import android.support.v4.content.pm.ShortcutManagerCompat
|
|||
import android.support.v4.graphics.drawable.IconCompat
|
||||
import android.view.*
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bumptech.glide.BitmapRequestBuilder
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||
import com.jakewharton.rxbinding.view.clicks
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
|
@ -33,15 +37,9 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
|||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.snack
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import jp.wasabeef.glide.transformations.CropCircleTransformation
|
||||
import jp.wasabeef.glide.transformations.CropSquareTransformation
|
||||
import jp.wasabeef.glide.transformations.MaskTransformation
|
||||
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.manga_info_controller.view.*
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subscriptions.Subscriptions
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
|
||||
|
@ -157,16 +155,16 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||
|
||||
// Set cover if it wasn't already.
|
||||
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(context)
|
||||
GlideApp.with(context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(manga_cover)
|
||||
|
||||
if (backdrop != null) {
|
||||
Glide.with(context)
|
||||
GlideApp.with(context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(backdrop)
|
||||
}
|
||||
|
@ -316,51 +314,78 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||
}
|
||||
|
||||
/**
|
||||
* Choose the shape of the icon
|
||||
* Only use for pre Oreo devices.
|
||||
* Add a shortcut of the manga to the home screen
|
||||
*/
|
||||
private fun chooseIconDialog() {
|
||||
val activity = activity ?: return
|
||||
private fun addToHomeScreen() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// TODO are transformations really unsupported or is it just the Pixel Launcher?
|
||||
createShortcutForShape()
|
||||
} else {
|
||||
ChooseShapeDialog(this).showDialog(router)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to choose a shape for the icon.
|
||||
*/
|
||||
private class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
constructor(target: MangaInfoController) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val modes = intArrayOf(R.string.circular_icon,
|
||||
R.string.rounded_icon,
|
||||
R.string.square_icon,
|
||||
R.string.star_icon)
|
||||
|
||||
val request = Glide.with(activity).load(presenter.manga).asBitmap()
|
||||
|
||||
fun getIcon(i: Int): Bitmap? = when (i) {
|
||||
0 -> request.transform(CropCircleTransformation(activity)).toIcon()
|
||||
1 -> request.transform(RoundedCornersTransformation(activity, 5, 0)).toIcon()
|
||||
2 -> request.transform(CropSquareTransformation(activity)).toIcon()
|
||||
3 -> request.transform(CenterCrop(activity),
|
||||
MaskTransformation(activity, R.drawable.mask_star)).toIcon()
|
||||
else -> null
|
||||
}
|
||||
|
||||
val dialog = MaterialDialog.Builder(activity)
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.icon_shape)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.items(modes.map { activity.getString(it) })
|
||||
.items(modes.map { activity?.getString(it) })
|
||||
.itemsCallback { _, _, i, _ ->
|
||||
Observable.fromCallable { getIcon(i) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ icon ->
|
||||
if (icon != null) createShortcut(icon)
|
||||
}, {
|
||||
activity.toast(R.string.icon_creation_fail)
|
||||
(targetController as? MangaInfoController)?.createShortcutForShape(i)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when
|
||||
* the resource is available.
|
||||
*
|
||||
* @param i The shape index to apply. No transformation is performed if the parameter is not
|
||||
* provided.
|
||||
*/
|
||||
private fun createShortcutForShape(i: Int = 0) {
|
||||
GlideApp.with(activity)
|
||||
.asBitmap()
|
||||
.load(presenter.manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.apply {
|
||||
when (i) {
|
||||
0 -> circleCrop()
|
||||
1 -> transform(RoundedCorners(5))
|
||||
2 -> transform(CropSquareTransformation())
|
||||
3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star))
|
||||
}
|
||||
}
|
||||
.into(object : SimpleTarget<Bitmap>(96, 96) {
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
createShortcut(resource)
|
||||
}
|
||||
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
activity?.toast(R.string.icon_creation_fail)
|
||||
}
|
||||
})
|
||||
}
|
||||
.show()
|
||||
|
||||
untilDestroySubscriptions.add(Subscriptions.create { dialog.dismiss() })
|
||||
}
|
||||
|
||||
private fun BitmapRequestBuilder<out Any, Bitmap>.toIcon() = this.into(96,96).get()
|
||||
|
||||
/**
|
||||
* Create shortcut using ShortcutManager.
|
||||
*
|
||||
* @param icon The image of the shortcut.
|
||||
*/
|
||||
private fun createShortcut(icon: Bitmap) {
|
||||
val activity = activity ?: return
|
||||
|
@ -375,49 +400,29 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||
|
||||
// Check if shortcut placement is supported
|
||||
if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
|
||||
val shortcutId = "manga-shortcut-${presenter.manga.title}-${presenter.source.name}"
|
||||
|
||||
// Create shortcut info
|
||||
val pinShortcutInfo = ShortcutInfoCompat.Builder(activity, "manga-shortcut-${presenter.manga.title}-${presenter.source.name}")
|
||||
val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId)
|
||||
.setShortLabel(presenter.manga.title)
|
||||
.setIcon(IconCompat.createWithBitmap(icon))
|
||||
.setIntent(shortcutIntent).build()
|
||||
.setIntent(shortcutIntent)
|
||||
.build()
|
||||
|
||||
val successCallback: PendingIntent
|
||||
|
||||
successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Create the CallbackIntent.
|
||||
val pinnedShortcutCallbackIntent = ShortcutManagerCompat.createShortcutResultIntent(activity, pinShortcutInfo)
|
||||
val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo)
|
||||
|
||||
// Configure the intent so that the broadcast receiver gets the callback successfully.
|
||||
PendingIntent.getBroadcast(activity, 0, pinnedShortcutCallbackIntent, 0)
|
||||
} else{
|
||||
PendingIntent.getBroadcast(activity, 0, intent, 0)
|
||||
} else {
|
||||
NotificationReceiver.shortcutCreatedBroadcast(activity)
|
||||
}
|
||||
|
||||
// Request shortcut.
|
||||
ShortcutManagerCompat.requestPinShortcut(activity, pinShortcutInfo,
|
||||
ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo,
|
||||
successCallback.intentSender)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shortcut of the manga to the home screen
|
||||
*/
|
||||
private fun addToHomeScreen() {
|
||||
// Get bitmap icon
|
||||
val bitmap = Glide.with(activity).load(presenter.manga).asBitmap()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
|
||||
Observable.fromCallable {
|
||||
bitmap.toIcon()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({icon ->
|
||||
createShortcut(icon)
|
||||
})
|
||||
}else{
|
||||
chooseIconDialog()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.ui.reader
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
|
@ -34,12 +34,12 @@ class SaveImageNotifier(private val context: Context) {
|
|||
* @param file image file containing downloaded page image.
|
||||
*/
|
||||
fun onComplete(file: File) {
|
||||
val bitmap = Glide.with(context)
|
||||
.load(file)
|
||||
val bitmap = GlideApp.with(context)
|
||||
.asBitmap()
|
||||
.load(file)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.into(720, 1280)
|
||||
.submit(720, 1280)
|
||||
.get()
|
||||
|
||||
if (bitmap != null) {
|
||||
|
|
|
@ -2,14 +2,13 @@ package eu.kanade.tachiyomi.ui.recent_updates
|
|||
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.setVectorCompat
|
||||
import jp.wasabeef.glide.transformations.CropCircleTransformation
|
||||
import kotlinx.android.synthetic.main.recent_chapters_item.view.*
|
||||
|
||||
/**
|
||||
|
@ -68,12 +67,12 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
|
|||
view.chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
|
||||
|
||||
// Set cover
|
||||
Glide.clear(itemView.manga_cover)
|
||||
GlideApp.with(itemView.context).clear(itemView.manga_cover)
|
||||
if (!item.manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(itemView.context)
|
||||
GlideApp.with(itemView.context)
|
||||
.load(item.manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.bitmapTransform(CropCircleTransformation(view.context))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.circleCrop()
|
||||
.into(itemView.manga_cover)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package eu.kanade.tachiyomi.ui.recently_read
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import kotlinx.android.synthetic.main.recently_read_item.view.*
|
||||
import java.util.*
|
||||
|
||||
|
@ -58,15 +58,15 @@ class RecentlyReadHolder(
|
|||
itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read))
|
||||
|
||||
// Set cover
|
||||
Glide.clear(itemView.cover)
|
||||
GlideApp.with(itemView.context).clear(itemView.cover)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(itemView.context)
|
||||
GlideApp.with(itemView.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(itemView.cover)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -5,9 +5,8 @@ import android.support.graphics.drawable.VectorDrawableCompat
|
|||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ImageView.ScaleType
|
||||
import com.bumptech.glide.load.resource.drawable.GlideDrawable
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.GlideDrawableImageViewTarget
|
||||
import com.bumptech.glide.request.target.ImageViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.gone
|
||||
|
@ -26,16 +25,23 @@ class StateImageViewTarget(view: ImageView,
|
|||
val progress: View? = null,
|
||||
val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp,
|
||||
val errorScaleType: ScaleType = ScaleType.CENTER) :
|
||||
GlideDrawableImageViewTarget(view) {
|
||||
|
||||
ImageViewTarget<Drawable>(view) {
|
||||
|
||||
private var resource: Drawable? = null
|
||||
|
||||
private val imageScaleType = view.scaleType
|
||||
|
||||
override fun setResource(resource: Drawable?) {
|
||||
view.setImageDrawable(resource)
|
||||
}
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
progress?.visible()
|
||||
super.onLoadStarted(placeholder)
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) {
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
progress?.gone()
|
||||
view.scaleType = errorScaleType
|
||||
|
||||
|
@ -49,9 +55,10 @@ class StateImageViewTarget(view: ImageView,
|
|||
super.onLoadCleared(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: GlideDrawable?, animation: GlideAnimation<in GlideDrawable>?) {
|
||||
override fun onResourceReady(resource: Drawable?, transition: Transition<in Drawable>?) {
|
||||
progress?.gone()
|
||||
view.scaleType = imageScaleType
|
||||
super.onResourceReady(resource, animation)
|
||||
super.onResourceReady(resource, transition)
|
||||
this.resource = resource
|
||||
}
|
||||
}
|
Reference in a new issue