This commit is contained in:
len 2017-10-14 18:16:11 +02:00
parent f45efe2aa8
commit 1470e9d5ca
20 changed files with 351 additions and 292 deletions

View File

@ -3,6 +3,7 @@ import java.text.SimpleDateFormat
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
if (file("custom.gradle").exists()) { if (file("custom.gradle").exists()) {
apply from: "custom.gradle" apply from: "custom.gradle"
@ -169,10 +170,12 @@ dependencies {
compile "uy.kohesive.injekt:injekt-core:1.16.1" compile "uy.kohesive.injekt:injekt-core:1.16.1"
// Image library // Image library
compile 'com.github.bumptech.glide:glide:3.8.0' compile 'com.github.bumptech.glide:glide:4.1.1'
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar' compile 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
kapt 'com.github.bumptech.glide:compiler:4.1.1'
// Transformations // Transformations
compile 'jp.wasabeef:glide-transformations:2.0.2' compile 'jp.wasabeef:glide-transformations:3.0.1'
// Logging // Logging
compile 'com.jakewharton.timber:timber:4.5.1' compile 'com.jakewharton.timber:timber:4.5.1'

View File

@ -24,6 +24,7 @@
# Glide specific rules # # Glide specific rules #
# https://github.com/bumptech/glide # https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.GlideModule -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$** { -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES; **[] $VALUES;
public *; public *;

View File

@ -95,10 +95,6 @@
android:name=".data.backup.BackupRestoreService" android:name=".data.backup.BackupRestoreService"
android:exported="false"/> android:exported="false"/>
<meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />
</application> </application>
</manifest> </manifest>

View File

@ -1,35 +1,51 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority import android.content.ContentValues.TAG
import com.bumptech.glide.load.data.DataFetcher import android.util.Log
import java.io.File import com.bumptech.glide.Priority
import java.io.IOException import com.bumptech.glide.load.DataSource
import java.io.InputStream import com.bumptech.glide.load.data.DataFetcher
import java.io.*
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
private var data: InputStream? = null
private var data: InputStream? = null
override fun loadData(priority: Priority): InputStream {
data = file.inputStream() override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
return data!! loadFromFile(callback)
} }
override fun cleanup() { protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
data?.let { data -> try {
try { data = FileInputStream(file)
data.close() } catch (e: FileNotFoundException) {
} catch (e: IOException) { if (Log.isLoggable(TAG, Log.DEBUG)) {
// Ignore Log.d(TAG, "Failed to open file", e)
} }
} callback.onLoadFailed(e)
} return
}
override fun cancel() {
// Do nothing. callback.onDataReady(data)
} }
override fun getId(): String { override fun cleanup() {
return file.toString() try {
} data?.close()
} catch (e: IOException) {
// Ignored.
}
}
override fun cancel() {
// Do nothing.
}
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
} }

View File

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

View File

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

View File

@ -1,23 +1,24 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.content.Context
import android.util.LruCache import android.util.LruCache
import com.bumptech.glide.Glide
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher 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.*
import com.bumptech.glide.load.model.stream.StreamModelLoader
import eu.kanade.tachiyomi.data.cache.CoverCache 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.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
/** /**
* A class for loading a cover associated with a [Manga] that can be present in our own cache. * 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 RAM LRU.
* - Check in disk LRU. * - Check in disk LRU.
@ -26,7 +27,7 @@ import java.io.InputStream
* *
* @param context the application context. * @param context the application context.
*/ */
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { class MangaModelLoader : ModelLoader<Manga, InputStream> {
/** /**
* Cover cache where persistent covers are stored. * Cover cache where persistent covers are stored.
@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
/** /**
* Base network loader. * Default network client.
*/ */
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java, private val defaultClient = Injekt.get<NetworkHelper>().client
InputStream::class.java, context)
/** /**
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url * 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. * 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. * Map where request headers are stored for a source.
@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
*/ */
class Factory : ModelLoaderFactory<Manga, InputStream> { class Factory : ModelLoaderFactory<Manga, InputStream> {
override fun build(context: Context, factories: GenericLoaderFactory) override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
= MangaModelLoader(context) return MangaModelLoader()
}
override fun teardown() {} 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. * 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 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. * @param height the height of the view where the resource will be loaded.
*/ */
override fun getResourceFetcher(manga: Manga, override fun buildLoadData(manga: Manga, width: Int, height: Int,
width: Int, options: Options?): ModelLoader.LoadData<InputStream>? {
height: Int): DataFetcher<InputStream>? {
// Check thumbnail is not null or empty // Check thumbnail is not null or empty
val url = manga.thumbnail_url val url = manga.thumbnail_url
if (url == null || url.isEmpty()) { if (url == null || url.isEmpty()) {
@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
if (url.startsWith("http")) { if (url.startsWith("http")) {
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
val glideUrl = GlideUrl(url, getHeaders(manga, source))
// 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)
}
// Get the resource fetcher for this request url. // Get the resource fetcher for this request url.
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) } val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
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 an instance of the fetcher providing the needed elements.
return MangaUrlFetcher(networkFetcher, file, manga) return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
} else { } else {
// Get the file from the url, removing the scheme if present. // Get the file from the url, removing the scheme if present.
val file = File(url.substringAfter("file://")) val file = File(url.substringAfter("file://"))
// Return an instance of the fetcher providing the needed elements. // 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
}
}
} }

View File

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

View File

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

View File

@ -1,12 +1,18 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder 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.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl 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.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -16,17 +22,20 @@ import java.io.InputStream
/** /**
* Class used to update Glide module settings * Class used to update Glide module settings
*/ */
class AppGlideModule : GlideModule { @GlideModule
class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) { override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 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) val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory) registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
} }
} }

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* 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) { override fun setImage(manga: Manga) {
Glide.clear(view.thumbnail) GlideApp.with(view.context).clear(view.thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop() .centerCrop()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(StateImageViewTarget(view.thumbnail, view.progress)) .into(StateImageViewTarget(view.thumbnail, view.progress))
} }
} }
} }

View File

@ -1,12 +1,11 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import jp.wasabeef.glide.transformations.CropCircleTransformation
import kotlinx.android.synthetic.main.catalogue_list_item.view.* 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) { override fun setImage(manga: Manga) {
Glide.clear(view.thumbnail) GlideApp.with(view.context).clear(view.thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop() .centerCrop()
.bitmapTransform(CropCircleTransformation(view.context)) .circleCrop()
.dontAnimate() .dontAnimate()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.* 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) { fun setImage(manga: Manga) {
Glide.clear(itemView.itemImage) GlideApp.with(itemView.context).clear(itemView.itemImage)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(itemView.context) GlideApp.with(itemView.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop() .centerCrop()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
/** /**
@ -38,10 +38,10 @@ class LibraryGridHolder(
} }
// Update the cover. // Update the cover.
Glide.clear(view.thumbnail) GlideApp.with(view.context).clear(view.thumbnail)
Glide.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(view.thumbnail) .into(view.thumbnail)
} }

View File

@ -1,11 +1,10 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga 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.* import kotlinx.android.synthetic.main.catalogue_list_item.view.*
/** /**
@ -46,12 +45,12 @@ class LibraryListHolder(
} }
// Update the cover. // Update the cover.
Glide.clear(itemView.thumbnail) GlideApp.with(itemView.context).clear(itemView.thumbnail)
Glide.with(itemView.context) GlideApp.with(itemView.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.bitmapTransform(CropCircleTransformation(itemView.context)) .circleCrop()
.dontAnimate() .dontAnimate()
.into(itemView.thumbnail) .into(itemView.thumbnail)
} }

View File

@ -1,8 +1,10 @@
package eu.kanade.tachiyomi.ui.manga.info package eu.kanade.tachiyomi.ui.manga.info
import android.app.Dialog
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -12,20 +14,22 @@ import android.support.v4.content.pm.ShortcutManagerCompat
import android.support.v4.graphics.drawable.IconCompat import android.support.v4.graphics.drawable.IconCompat
import android.view.* import android.view.*
import com.afollestad.materialdialogs.MaterialDialog 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.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.support.v4.widget.refreshes
import com.jakewharton.rxbinding.view.clicks import com.jakewharton.rxbinding.view.clicks
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
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
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource 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.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.main.MainActivity 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.getResourceColor
import eu.kanade.tachiyomi.util.snack import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import jp.wasabeef.glide.transformations.CropCircleTransformation
import jp.wasabeef.glide.transformations.CropSquareTransformation import jp.wasabeef.glide.transformations.CropSquareTransformation
import jp.wasabeef.glide.transformations.MaskTransformation import jp.wasabeef.glide.transformations.MaskTransformation
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.manga_info_controller.view.* 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 uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat import java.text.DecimalFormat
@ -157,16 +155,16 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// Set cover if it wasn't already. // Set cover if it wasn't already.
if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(context) GlideApp.with(context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(manga_cover) .into(manga_cover)
if (backdrop != null) { if (backdrop != null) {
Glide.with(context) GlideApp.with(context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(backdrop) .into(backdrop)
} }
@ -316,51 +314,78 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
} }
/** /**
* Choose the shape of the icon * Add a shortcut of the manga to the home screen
* Only use for pre Oreo devices.
*/ */
private fun chooseIconDialog() { private fun addToHomeScreen() {
val activity = activity ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// TODO are transformations really unsupported or is it just the Pixel Launcher?
val modes = intArrayOf(R.string.circular_icon, createShortcutForShape()
R.string.rounded_icon, } else {
R.string.square_icon, ChooseShapeDialog(this).showDialog(router)
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)
.title(R.string.icon_shape)
.negativeText(android.R.string.cancel)
.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)
})
}
.show()
untilDestroySubscriptions.add(Subscriptions.create { dialog.dismiss() })
} }
private fun BitmapRequestBuilder<out Any, Bitmap>.toIcon() = this.into(96,96).get() /**
* 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)
return MaterialDialog.Builder(activity!!)
.title(R.string.icon_shape)
.negativeText(android.R.string.cancel)
.items(modes.map { activity?.getString(it) })
.itemsCallback { _, _, i, _ ->
(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)
}
})
}
/** /**
* Create shortcut using ShortcutManager. * Create shortcut using ShortcutManager.
*
* @param icon The image of the shortcut.
*/ */
private fun createShortcut(icon: Bitmap) { private fun createShortcut(icon: Bitmap) {
val activity = activity ?: return val activity = activity ?: return
@ -375,49 +400,29 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// Check if shortcut placement is supported // Check if shortcut placement is supported
if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) { if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
val shortcutId = "manga-shortcut-${presenter.manga.title}-${presenter.source.name}"
// Create shortcut info // 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) .setShortLabel(presenter.manga.title)
.setIcon(IconCompat.createWithBitmap(icon)) .setIcon(IconCompat.createWithBitmap(icon))
.setIntent(shortcutIntent).build() .setIntent(shortcutIntent)
.build()
val successCallback: PendingIntent val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create the CallbackIntent. // 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. // Configure the intent so that the broadcast receiver gets the callback successfully.
PendingIntent.getBroadcast(activity, 0, pinnedShortcutCallbackIntent, 0) PendingIntent.getBroadcast(activity, 0, intent, 0)
} else{ } else {
NotificationReceiver.shortcutCreatedBroadcast(activity) NotificationReceiver.shortcutCreatedBroadcast(activity)
} }
// Request shortcut. // Request shortcut.
ShortcutManagerCompat.requestPinShortcut(activity, pinShortcutInfo, ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo,
successCallback.intentSender) 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()
}
}
}

View File

@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.ui.reader
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.kanade.tachiyomi.R 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.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications 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. * @param file image file containing downloaded page image.
*/ */
fun onComplete(file: File) { fun onComplete(file: File) {
val bitmap = Glide.with(context) val bitmap = GlideApp.with(context)
.load(file)
.asBitmap() .asBitmap()
.load(file)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true) .skipMemoryCache(true)
.into(720, 1280) .submit(720, 1280)
.get() .get()
if (bitmap != null) { if (bitmap != null) {

View File

@ -2,14 +2,13 @@ package eu.kanade.tachiyomi.ui.recent_updates
import android.view.View import android.view.View
import android.widget.PopupMenu import android.widget.PopupMenu
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download 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.getResourceColor
import eu.kanade.tachiyomi.util.setVectorCompat import eu.kanade.tachiyomi.util.setVectorCompat
import jp.wasabeef.glide.transformations.CropCircleTransformation
import kotlinx.android.synthetic.main.recent_chapters_item.view.* 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)) view.chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color))
// Set cover // Set cover
Glide.clear(itemView.manga_cover) GlideApp.with(itemView.context).clear(itemView.manga_cover)
if (!item.manga.thumbnail_url.isNullOrEmpty()) { if (!item.manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(itemView.context) GlideApp.with(itemView.context)
.load(item.manga) .load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.bitmapTransform(CropCircleTransformation(view.context)) .circleCrop()
.into(itemView.manga_cover) .into(itemView.manga_cover)
} }

View File

@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.ui.recently_read package eu.kanade.tachiyomi.ui.recently_read
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory 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 kotlinx.android.synthetic.main.recently_read_item.view.*
import java.util.* import java.util.*
@ -58,15 +58,15 @@ class RecentlyReadHolder(
itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read)) itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read))
// Set cover // Set cover
Glide.clear(itemView.cover) GlideApp.with(itemView.context).clear(itemView.cover)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(itemView.context) GlideApp.with(itemView.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(itemView.cover) .into(itemView.cover)
} }
} }
} }

View File

@ -5,9 +5,8 @@ import android.support.graphics.drawable.VectorDrawableCompat
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.ImageView.ScaleType import android.widget.ImageView.ScaleType
import com.bumptech.glide.load.resource.drawable.GlideDrawable import com.bumptech.glide.request.target.ImageViewTarget
import com.bumptech.glide.request.animation.GlideAnimation import com.bumptech.glide.request.transition.Transition
import com.bumptech.glide.request.target.GlideDrawableImageViewTarget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.gone
@ -26,16 +25,23 @@ class StateImageViewTarget(view: ImageView,
val progress: View? = null, val progress: View? = null,
val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp, val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp,
val errorScaleType: ScaleType = ScaleType.CENTER) : val errorScaleType: ScaleType = ScaleType.CENTER) :
GlideDrawableImageViewTarget(view) {
ImageViewTarget<Drawable>(view) {
private var resource: Drawable? = null
private val imageScaleType = view.scaleType private val imageScaleType = view.scaleType
override fun setResource(resource: Drawable?) {
view.setImageDrawable(resource)
}
override fun onLoadStarted(placeholder: Drawable?) { override fun onLoadStarted(placeholder: Drawable?) {
progress?.visible() progress?.visible()
super.onLoadStarted(placeholder) super.onLoadStarted(placeholder)
} }
override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) { override fun onLoadFailed(errorDrawable: Drawable?) {
progress?.gone() progress?.gone()
view.scaleType = errorScaleType view.scaleType = errorScaleType
@ -49,9 +55,10 @@ class StateImageViewTarget(view: ImageView,
super.onLoadCleared(placeholder) super.onLoadCleared(placeholder)
} }
override fun onResourceReady(resource: GlideDrawable?, animation: GlideAnimation<in GlideDrawable>?) { override fun onResourceReady(resource: Drawable?, transition: Transition<in Drawable>?) {
progress?.gone() progress?.gone()
view.scaleType = imageScaleType view.scaleType = imageScaleType
super.onResourceReady(resource, animation) super.onResourceReady(resource, transition)
this.resource = resource
} }
} }